July 30, 2026
7 min read
backend / software-architecture / system-design
Monoliths vs Microservices: Follow One Order Across the Boundary
Microservices relocate complexity rather than remove it. Following a single order, with stock and payment, from one database transaction to three services shows where that complexity goes and when it is worth paying for.

A customer places an order. The system has to create the order, reserve the stock and charge the payment, and if any of those fails, none of them should stick.
In a monolith with one relational database, that requirement is a transaction. Split orders, inventory and payments into three services with their own databases, and the same requirement turns into a protocol you have to design, test and operate.
That change is the core of the monolith versus microservices decision, and it is easy to lose in comparisons framed as old versus modern. This post follows that one order through both architectures to show where the complexity goes, then asks when the move pays for itself. It is a design analysis built on published patterns, not a report from a specific migration.
One order, one transaction
In a monolith, the three steps are function calls inside one process, sharing one database connection:
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (...);
UPDATE stock SET reserved = reserved + 1 WHERE sku = 'A-17' AND available - reserved >= 1;
INSERT INTO payments (order_id, amount, status) VALUES (..., 'PENDING');
COMMIT;
If the stock update matches zero rows, the code rolls back and nothing happened. If the process crashes halfway, the database rolls back the open transaction. A reader never sees an order without its reservation.
The external card charge is the one step a database transaction cannot cover. Whether the charge happens before or after the commit, the two cannot be made atomic, and a monolith still has to handle one succeeding while the other fails. Everything internal, though, is atomic. Debugging is a single stack trace. A test can call the whole flow in-process.
That is what "monolith" should mean in this comparison: one deployment unit and, usually, one transactional store. It does not mean "messy". A monolith becomes hard to change when its modules reach into each other's tables and internals, which is a boundary problem. Splitting the system across a network will not fix that.
The same order across three services
Now give orders, inventory and payments their own services and databases. The order service receives the request and has to coordinate the other two over the network.
The first call, reserve stock, can be refused (no stock), it can fail (inventory is down), or it can time out. The first two have local equivalents. The third is new: an outcome you cannot know. A timeout is the hard case: the order service does not know whether the reservation happened. Retrying may reserve twice. Not retrying may leave a reservation nobody will release.
The usual answer is an idempotency key: the order ID travels with every call, and inventory records which order IDs it has already handled, so a retry returns the original result instead of reserving again. That is a table, a uniqueness constraint and a test suite inventory did not need before.
The second call, charge the payment, raises the next question. If payment is declined after stock was reserved, there is no shared ROLLBACK. The order service has to send a compensating action, "release reservation for order 123", which can itself time out and be retried. This sequence of local transactions plus compensations is the Saga pattern. It works, but between steps the system is visibly inconsistent: an order can exist with stock reserved and payment pending. The product has to define what customers and support staff see in that state.
The last problem is publishing. The order service updates its database and then emits "order placed" for other services. If it crashes between those two steps, the database says one thing and the rest of the system never hears about it. The Transactional Outbox pattern fixes this by writing the event into the same local transaction and relaying it afterwards. Consumers then need to tolerate duplicates, because the relay delivers at least once.
None of these steps is exotic. Each one replaces a guarantee the database used to provide with code the team now owns.
What the split buys
That cost can be worth paying, under specific pressures:
- Independent deployment. If the payments team ships several times a day and every release currently waits on a shared build and test cycle, separate services remove that coordination.
- Different scaling profiles. If catalog browsing receives orders of magnitude more traffic than checkout, scaling them separately avoids paying for checkout capacity you do not use.
- Failure isolation. If email sending slows down, an asynchronous boundary keeps order creation working.
Each benefit depends on the boundary following a business capability with few synchronous dependencies. Orders, inventory and payments are reasonable candidates. "Controllers", "services" and "repositories" are technical layers, and splitting along them produces services that still have to change together.
The costs that arrive with the fifth service
The first service extraction looks cheap. The cost grows with the count: each new service brings its own pipeline, configuration, secrets, service-to-service authentication, dashboards and alerts, and its own contract versions to keep compatible. Following one request now requires distributed tracing. Reproducing a bug locally means running several services, or faking them convincingly.
A team adopting microservices ends up building an internal platform, whether or not it plans to. Martin Fowler calls this the microservice premium: a cost in productivity that only pays off once the system is complex enough to need what microservices provide.
The worst outcome pays the premium and gets nothing back. In a distributed monolith, services are separate on paper but every meaningful change still touches several of them, ships in a coordinated release and fails together. The team carries network failure modes without gaining deployment independence.
Where CAP actually enters
The CAP theorem shows up in these discussions as an argument for microservices. It is a constraint that applies once state is distributed: during a network partition, a component has to choose between answering with possibly stale data and refusing to answer.
In the order example, that choice differs per capability. Stock reservation should refuse rather than reserve against a stale count and oversell. A product catalog can keep serving slightly old prices for a few seconds. Once the system is distributed, those choices stop being infrastructure details. Someone has to write them down as product behavior.
A modular monolith first, with a test for extraction
For most teams still learning their domain, I would start with a modular monolith. In the order example, orders, inventory and payments would be modules with explicit interfaces, no access to each other's tables, and dependency rules the build enforces. The flow still commits in one transaction. The boundaries are already visible in code.
That arrangement keeps the transaction while making a later extraction mechanical rather than archaeological. When a real pressure appears, the Strangler Fig approach extracts one module behind its existing interface and routes traffic to it gradually. That limits the blast radius but does not make extraction free: there is a period of parallel paths and compatibility work.
Before extracting a module, I would want a clear answer to three questions:
- Which specific pressure does this solve: deployment coordination, scaling, or failure isolation? Can I point to where it hurts today?
- Is the module's boundary stable, with few synchronous calls into other modules?
- Are we ready to own the replacement for each guarantee we lose, including idempotency, compensations, an outbox, tracing and contract versioning?
If the first answer is vague, the problem is probably inside the monolith's boundaries, and fixing them costs less than adding a network between them. Fowler's Monolith First argues for the same starting point. The practical habit I would add is smaller: before moving a flow like this order across a network, trace it step by step and list every guarantee you are about to rebuild by hand.