Spaghetti (Anti-Pattern)
“‘O spaghetto è buono ‘o piatto — dint’o codice è ‘na disgrazia.” (Spaghetti is excellent on a plate — in the code it’s a disaster.)
We document the Spaghetti anti-pattern because it emerges naturally when developers are under pressure, skipping planning, or “just wiring things together quickly.” Understanding what it looks like — and why it is catastrophic — is the first step toward never writing it again.
What is Spaghetti Code?
Spaghetti code is code in which every module depends on every other module, responsibilities are tangled together, and following the execution path requires tracking a dozen different files simultaneously. Like a plate of spaghetti: tug one strand and every other strand moves. There are no clean ends. There are no seams. There is only the tangle.
In distributed systems, Spaghetti is especially dangerous because the tangling spans not just files, but network boundaries, database transactions, and event handlers. A Spaghetti microservice is not a microservice — it is a distributed monolith with extra suffering.
Anatomy of a Spaghetti Disaster
Here is a real-looking example of Spaghetti code in a Pasta Protocol kitchen. Read it carefully. Feel the horror.
// ⚠️ DO NOT WRITE CODE LIKE THIS ⚠️// This handler does seventeen things. Name one of them.
import { db } from '../../globals/database'; // global mutable singletonimport { mailer } from '../../globals/mailer'; // another globalimport { stripe } from '../../globals/payments'; // and anotherimport { logger } from '../../globals/logger';import { cache } from '../../globals/cache';
export async function handleOrdine(req: any, res: any) { // Parse, validate, query, charge, email, cache — all in one function const body = JSON.parse(req.body); // no schema validation const cliente = await db.query(`SELECT * FROM clienti WHERE id = '${body.clienteId}'`); // ↑ SQL injection waiting to happen. TERREMOTO.
if (!cliente.rows[0]) { res.status(404).send('not found'); // leaking DB structure to HTTP return; }
// Business logic tangled directly with HTTP handling if (cliente.rows[0].credito < body.importo) { mailer.send(cliente.rows[0].email, 'Credito insufficiente'); // side-effect inside validation res.status(402).send('credito insufficiente'); return; }
// Mutate a shared object instead of creating a new one cliente.rows[0].credito -= body.importo; // mutating a DB result object await db.query( `UPDATE clienti SET credito = ${cliente.rows[0].credito} WHERE id = '${body.clienteId}'`, ); // ↑ SQL injection again. No transaction. If the next step fails, the credit is gone forever.
const chargeResult = await stripe.charge({ amount: body.importo, customer: cliente.rows[0].stripeId, }); // No error handling. If Stripe is down: TERREMOTO, unhandled rejection.
// Email notification inside the same transaction path await mailer.send(cliente.rows[0].email, `Ordine confermato: ${chargeResult.id}`);
// Cache invalidation logic mixed with business logic cache.del(`cliente:${body.clienteId}`); cache.del(`ordini:${body.clienteId}`);
// Direct database ID exposed in the API response res.status(200).json({ ordineId: chargeResult.id, clienteDbId: cliente.rows[0].id }); // ↑ Leaked internal structure. Every consumer is now coupled to your schema.}Count the violations:
- SQL injection — twice. Unparameterized queries constructed with string interpolation.
- No input validation —
req.bodyis cast withJSON.parseand used directly. - Mutation —
cliente.rows[0].creditois mutated in place. - No transaction — credit is deducted before the charge. A Stripe failure leaves an inconsistent state.
- Tangled concerns — HTTP, business logic, payment, email, and cache in a single 40-line function.
- Global mutable singletons — impossible to test in isolation.
- Unhandled promise rejection — Stripe failure crashes the process.
- Internal structure leakage — database IDs in the HTTP response.
- Side-effects inside validation — email sent before the full validation is complete.
- No error levels — every failure is either a
404or an unhandled crash.
Why It Feels Fast (And Why It Isn’t)
The first time you write code like this, it works. The feature ships in an afternoon. You feel productive. ‘O capo è contento.
Then the second feature arrives. You can’t reuse the Stripe logic because it’s fused to the mailer. You can’t test the credit-check logic because it requires a live database. You can’t change the database schema because three other functions do cliente.rows[0].id. Every change requires understanding the whole tangle before touching any single strand.
After six months, every developer on the team is afraid to change anything. The codebase has become a distributed TERREMOTO in slow motion.
The Corrected Architecture
Replace the spaghetti with a Lasagna (layered) architecture feeding into a Rigatoni pipeline:
- Besciamella layer: parse the request, validate with a schema, dispatch a
CreaOrdineCommand. - Ragù layer: apply domain rules (credit check), produce a
OrdineConvalidatodomain object. - Pasta Sfoglia layer: persist via a repository, charge via a
PagamentiPort, enqueue email via aNotifichePort.
Each piece is independently testable, independently deployable, and independently comprehensible. See Lasagna Architecture and Rigatoni Pipeline for the correct approach.