Monolithic Architecture
A monolith is one deployable process containing every business capability — auth, users, orders, payments, notifications — sharing one database and one transaction scope; it is the cheapest architecture to build, run and debug, scales further than its reputation suggests, and is not automatically bad.
A product needs auth, users, orders, payments and notifications to work together, with correctness across them (an order and its payment succeed or fail together) and a team small enough to share one codebase. A monolith gives all of that with one deploy, one database and no network between the parts.
What a monolith is
A monolith is a single process — one build artifact, one deploy — that contains every business capability of the application. Inside it, auth, users, orders, payments and notifications are packages or modules; they call each other with ordinary function calls, share one connection pool to one database, and can join each other’s tables in one query. From outside, it is one HTTP server behind one load balancer.
The word is often used as an insult, and that is a mistake worth correcting explicitly: a monolith is not automatically bad architecture. It is the default shape of most successful systems in their first years, and of many at very large scale. Shopify, Stack Overflow and GitHub ran (and in significant part still run) as monoliths at traffic most teams will never see. What makes a monolith bad is unmanaged coupling inside it, and that is a code-structure problem, not a consequence of the deployment shape.
Why it is a good default
The advantages are concrete and they compound. Simple deployment: one artifact, one pipeline, one rollback; a release is a version number, not a compatibility matrix between nine services. Easy local development: docker compose up starts one service and one database, and a new engineer runs the whole product on a laptop in an hour. Easy transactions: placing an order, reserving stock and recording the payment happen inside one BEGIN … COMMIT; if the payment insert fails, the order insert rolls back, and there is no saga, no compensation and no "pending" state to explain to the user (Transactions and ACID).
Fewer distributed-systems problems: a function call costs about 50 ns and cannot time out, be retried into a duplicate, or return a partial result; a service call costs 1–5 ms and can do all three. A monolith has no service discovery, no inter-service authentication, no schema versioning between deployables, and one log stream that a grep can follow end to end. Lower operational cost: one thing to monitor, one on-call rotation, one set of dashboards; the platform team for a monolith is often nobody.
1async function placeOrder(userId: string, cart: Cart): Promise<Order> {2 return db.transaction(async (tx) => {3 const order = await orders.create(tx, { userId, lines: cart.lines, status: 'pending' })4 await inventory.reserve(tx, cart.lines) // same connection, same tx5 const charge = await payments.charge(tx, order) // fails → everything rolls back6 await orders.markPaid(tx, order.id, charge.id)7 notifications.enqueueAfterCommit(tx, 'order-paid', order.id)8 return order9 })10}Where it hurts
The disadvantages are real, and they arrive with size — of the codebase and of the organisation. A large codebase: at 500k lines, a full build takes 25 minutes and the test suite 40, so a one-line fix has a one-hour feedback loop. Coupled deployments: five teams ship in one release, so a bug in a growth experiment rolls back a payments fix, and the release cadence slows to whatever the most cautious team accepts. Harder team boundaries at scale: nothing stops the orders package from reaching into the payments tables, and after three years everything depends on everything — the "big ball of mud".
Scaling inefficiencies: the process is copied whole. If only PDF report generation is CPU-heavy, every instance still carries it, and a memory leak in reporting takes down checkout on the same box, because they share a failure domain. A monolith cannot give the payments team a different runtime, a different release cadence or a different scaling curve from the catalogue team. When those are the problems you are measuring, the shape is the constraint — and the next lesson, Modular Monolith, fixes most of them without leaving the process.
- Build and test time grow with the codebase; the feedback loop is the first thing teams feel.
- One release couples every team; the cadence converges on the slowest.
- Without enforced boundaries, cross-module coupling grows until refactoring is unsafe.
- Scaling is whole-process: a hot path cannot be scaled without the cold ones; one failure domain for all capabilities.
How a monolith scales
A monolith scales horizontally the same way a service does: N identical copies behind a load balancer. The precondition is that the process is stateless — sessions, upload progress and caches live in Redis or the database, not in process memory — so that any copy can serve any request and copies can be added or killed at will (Stateless vs Stateful Services). With that in place, ten copies give roughly ten times the request capacity, and a rolling deploy replaces them one at a time with zero downtime.
The database is the ceiling. Every copy talks to the same Postgres, so the limit is not the application but the database’s CPU, connections and write throughput. The response is the database ladder: a connection pooler, read replicas for read traffic, a cache for hot keys, partitioning of the largest tables — Scaling from One User to Millions. A monolith with a well-scaled database serves tens of thousands of requests per second. Only when the write ceiling of a single primary is reached, or when the organisational costs above dominate, does the application shape itself need to change.
Key points
- One process, one deploy, one database, one transaction scope: the cheapest architecture to build, run and debug.
- A monolith is not automatically bad; unmanaged internal coupling is the actual problem, and it is a code-structure problem.
- Function calls cost nanoseconds and cannot partially fail; the monolith has no timeouts, retries, discovery or contract versioning between its parts.
- It scales horizontally as stateless copies behind a load balancer; the database is the ceiling and has its own ladder.
- Leave it for measured reasons — build time, deploy coupling, differing scaling needs — and go through a modular monolith first.
One request through a monolith
http.handle(POST /orders)
How data moves through it
One request or event, hop by hop.
- 1Browser → Load balancer → Monolith:
POST /orderslands on any copy; the copy reads the session from Redis. - 2Monolith (Orders) → Monolith (Payments): a function call inside the same process, sharing the open transaction.
- 3Monolith → Postgres: order, stock reservation and charge are written in one transaction; commit is atomic.
- 4Monolith (Notifications) → Queue: an after-commit hook enqueues the confirmation email so the request does not wait on the email provider.
- 5Monolith → Browser:
201 Createdwith the order; no pending states, because everything either committed or rolled back.
When to use — and when not
- A new product or a team of up to a few dozen engineers: the domain boundaries are not yet known, and one deployable lets them be discovered cheaply.
- Workloads where correctness across capabilities matters more than independent scaling — orders, stock and payment in one transaction.
- Organisations without a platform team: one service to run means the product team can own operations.
- Several teams measurably blocked on one release train (deploy lead time in days, rollbacks that undo other teams’ work).
- Components with genuinely different scaling or runtime needs — GPU inference next to a CRUD API — where copying the whole process wastes most of it.
- When the single primary database’s write throughput is the measured limit and the data can be split along a business boundary.
Tradeoffs
Lowest complexity and operational cost of any shape, and the strongest consistency because one database holds everything. Scalability is a 3, not a 1: horizontal copies go a long way, and the ceiling is the database, not the monolith.
How it fails
- Shared failure domain: a memory leak in report generation takes down checkout on the same instance, because both run in one process.
- Coupled release: a rollback of one team’s bad deploy also removes another team’s fix shipped in the same artifact.
- Big ball of mud: with no enforced boundaries, the orders package reads payments tables directly; a payments schema migration breaks orders in production.
- Connection exhaustion: 30 instances × 50 pooled connections each opens 1,500 Postgres connections, and the database, not the app, falls over first.
- Stateful copies: sessions in process memory plus autoscaling logs users out at random.
How it scales
- Horizontally: stateless copies behind a load balancer, with sessions and caches externalised to Redis; rolling deploys replace copies one at a time.
- Vertically first for the database: a larger instance with the working set in RAM changes no arrows and adds no failure modes.
- Then the database ladder: pooler, read replicas, cache, partitioning — the monolith itself rarely needs to change for any of these.
- The next shape change is a modular monolith (boundaries) or extracting one service with a different scaling curve, not a wholesale rewrite.
How it interacts with databases, queues, caches, APIs and external systems
- Database: one schema, one pool, one transaction scope; every module can join any table, which is both the power and the coupling risk.
- Cache: Redis for sessions and hot reads, the precondition for running more than one copy.
- Queue: still useful inside a monolith for slow work (email, PDFs); the consumer is the same codebase running in worker mode.
- APIs: one HTTP surface; internal capabilities are never exposed as separate APIs, so there is no inter-service auth or versioning.
- External systems: the payment provider is called from the payments module with a timeout; a webhook endpoint in the same process receives confirmations.