All articles

architecture

When to use a monolith instead of microservices

A guide for developers a few years in: what you gain and pay with each architecture, what Segment, Shopify and Prime Video found, and the questions to ask before you split anything.

Victor K Kiptum

5 min read

An online shop drawn twice: orders, payments and shipping inside one unit with one database, and as three separate services, each with its own database, joined by network calls

Around three years into the job, the questions change. You stop being asked how to build a feature and start being asked where it should live. "Should this be its own service?" comes up in design reviews and interviews, and "microservices scale better" does not survive a follow-up question.

The short answer

Start with a monolith, keep its internals well separated, and split out a service only when you can name the problem the split solves.

That is close to Martin Fowler's position in his article on microservice trade-offs: "if you can manage your system's complexity with a monolithic architecture then you shouldn't be using microservices."

The difference

Take an online shop with orders, payments and shipping. In a monolith, all three live in one application, deployed as one unit, usually with one database. In a microservice design, each is its own application. James Lewis and Martin Fowler's original article defines the style as "suites of independently deployable services".

AspectMonolithMicroservices
DeploymentOne unitOne per service
Calling other partsFunction callNetwork call
DataOne databaseOne per service
ScalingThe whole applicationEach service separately

Fowler names three benefits of microservices: strong module boundaries, independent deployment, and freedom to use different technology per service. He names three costs. Remote calls "are slow and are always at risk of failure". Data split across services loses the single transaction that keeps it consistent. And "you need a mature operations team to manage lots of services".

There is an option in between. A modular monolith keeps one deployment but enforces boundaries inside it. Shopify describes its own as "a single application" with "strictly enforced boundaries between different domains". That gets you the first benefit without the three costs.

What the costs look like

Placing an order means saving it, taking payment and reserving stock. This sketch is illustrative, not from a real codebase.

// Monolith: one transaction. If any step throws, all three roll back.
await db.transaction(async (tx) => {
  const order = await orders.create(tx, cart);
  await payments.charge(tx, order);
  await stock.reserve(tx, order);
});

// Microservices: three network calls. Nothing rolls back for you.
const order = await orderService.create(cart);
await paymentService.charge(order); // timed out: was the customer charged?
await stockService.reserve(order);  // failed: now refund the payment

In the second version, retries must be safe to run twice, and a failure halfway needs code that undoes the earlier steps. The monolith needed neither.

I worked on a point-of-sale and inventory system for shops. It is a modular monolith: one Django application and one PostgreSQL database, with sales, inventory and payments as separate modules. Recording a sale is a single transaction. It saves the sale, locks each product row, checks and deducts the stock, records the payment and issues the receipt. If the third item in a basket turns out to be out of stock, or the payment is rejected, the whole sale rolls back. No shop ends up with a sale on the books whose stock was never deducted. With sales and inventory as separate services, that guarantee would be code I had to write and test myself.

What teams found

Three teams that reconsidered their architecture

TeamStarting pointWhat they changedReported result
Segment (2018)More than 140 services run by three engineersMerged them into one serviceDeploys in minutes; weaker fault isolation
Prime Video (2023)Serverless monitoring tool hitting account limitsPacked the components into one processInfrastructure cost down over 90%
Shopify (2019)A large monolith that had become fragileKept one deployment, enforced internal boundariesNot disclosed

Sources: a post by engineer Alexandra Noonan for Segment; DevClass for Prime Video, whose figures cover one service and not all of Prime Video; Shopify's engineering post, linked above, for Shopify.

None of these teams concluded that microservices are bad. Each found that one design did not fit one problem. Fowler's Monolith First sums up the pattern: "Almost all the successful microservice stories have started with a monolith that got too big and was broken up."

When microservices are the right call

Look for a named pressure, not a general wish to scale:

  • Several teams keep blocking each other in one codebase.
  • One part has a very different load from the rest.
  • One part's failures must not take down the others.

Two mistakes are worth avoiding. Do not split before you know the boundaries: a wrong boundary inside a monolith is a refactor, and between services it is a migration. And do not let services share a database or deploy together, which pays the network cost for none of the independence.

Takeaways

  • A monolith is the default, and a modular one stays workable for a long time.
  • Microservices trade simple code for independent teams. That only pays when you have the teams.
  • "It depends" is a fine interview answer, provided you can say what it depends on.

My own position: The system has grown to more than a dozen modules in one deployment, and I have not yet met a problem there that a separate service would solve. I would start my next system the same way, and split only when one of the pressures above shows up.

Sources

architecturemicroservicesbackend

Written by

Victor K Kiptum

Software Engineer · About me

More articles