mosaddik.dev
All posts

Monolith vs microservices: which architecture should you choose?

Mosaddik Billah 5 min read

Software Engineering

A woman drawing a system diagram on a whiteboard
Photo by Beatriz Cattel on Unsplash
On this page
  1. What a monolith gives you
  2. What microservices give you, and what they cost
  3. The middle path: a modular monolith
  4. When to split something out
  5. A rough guide

A monolith is one application: one codebase, deployed as one unit, usually with one database. Microservices split the system into many small applications, each owning one part of the business (orders, payments, emails), deployed separately and talking over the network.

Microservices get a lot of attention because large tech companies use them. But they solve problems that most teams don’t have yet, and they bring problems that every team using them has. Here’s how to tell which you need.

What a monolith gives you#

  • Simple to build and run. One app to start, one to deploy, one set of logs to read.
  • Fast, reliable calls. One part of the code calls another as a function call: no network, no timeouts, no retries.
  • Transactions just work. Creating an order and reducing stock can happen in one database transaction, so they either both happen or neither does.
  • Easy to change. Renaming something used in ten places is one search and one deploy.
  • Easy to debug. A request’s whole story is in one place.

The downsides arrive with size: a big codebase can become tangled if nobody keeps it organised, every change deploys everything, and many teams working in one codebase get in each other’s way.

What microservices give you, and what they cost#

The benefits are real:

  • Teams deploy independently. The payments team can ship without coordinating with the search team.
  • Parts scale independently. The image-processing service can run on 50 machines while the admin service runs on one.
  • Failures can be contained. If recommendations are down, checkout can keep working, if it’s designed to.
  • Different tools for different jobs. One service can use a different language or database where it really helps.

So are the costs, and they’re paid from day one:

  • Every call can fail. A function call that always worked is now a network request that can be slow, time out or fail halfway. You need timeouts, retries, idempotency and fallbacks everywhere.
  • No more simple transactions. Order and stock live in different databases, so “both or neither” needs patterns like sagas and the outbox, which are far harder to get right.
  • Much more infrastructure. Each service needs deployment, monitoring, logs, secrets and on-call. You need tracing just to follow one request through ten services.
  • Changes that cross services get slower, because several services, teams and deploys must agree on the change and its timing.
  • Testing the whole system is harder, because it only exists when everything is running together.

The split is rarely as clean as the diagram. Done badly, you get a distributed monolith: services that must all be deployed together and fail together, with the costs of both approaches and the benefits of neither.

The middle path: a modular monolith#

For most projects, the best start is a modular monolith: one deployable application, organised internally into clear modules with boundaries.

src/
  orders/      routes, logic and data access for orders
  payments/    the same for payments
  catalogue/
  customers/
  shared/      small, truly shared helpers

The rules that make it work:

  • Modules talk through a small public interface, not by reaching into each other’s internals or tables. orders calls payments.charge(), never UPDATE payments ....
  • Each module owns its data. Other modules ask through its interface.
  • Dependencies point one way. If orders uses catalogue, catalogue doesn’t also use orders.

You get the simplicity of one app, and if a module ever does need to become its own service, the boundary is already there; you’re replacing function calls with network calls rather than untangling spaghetti. How to structure a backend project shows the inside of a module.

When to split something out#

Split off a service when you can name a specific, current problem it solves. Good reasons:

  • A team boundary. Separate teams own clearly separate areas and are blocking each other’s releases.
  • Very different scaling needs. One part needs far more machines, or different hardware such as GPUs.
  • Different reliability needs. The checkout must stay up even when the reporting engine is overloaded.
  • A hard isolation requirement, such as keeping card data in a small, separately audited service.

Weak reasons: “it’s more modern”, “Netflix does it”, “so we can use a different language”, or “the code is messy”. A messy monolith split into services becomes a messy distributed system.

When you do split, split one piece at a time, starting with the part that has the clearest boundary and the strongest reason. Put it behind the same interface the rest of the code already used, and keep the monolith running alongside. This gradual approach is often called the strangler fig pattern.

A rough guide#

Situation Choose
New product, small team, requirements still changing Monolith (modular)
One team, up to a few dozen developers Modular monolith
Several teams blocking each other on releases Split along team boundaries
One component with very different load or hardware Split that component only
Large organisation with platform and on-call teams Microservices can pay off

Architecture is easier to change than people fear if the code has clear boundaries, and much harder if it doesn’t. Start simple, keep it organised, and split when the pain is real. For growth that doesn’t need a new architecture, see how to scale a web app.

If you’re planning a new app or deciding how to extend an existing one, I can help choose and build the right structure as part of my Wix apps with the Wix CLI and custom API integration work.

Comments