azyware
Business

Microservices vs a modular monolith: which to choose and when

EZ
Eazyware
· 7 min read
Quick answer

What is the difference between microservices and a modular monolith?

Deployment independence is the main thing microservices buy that a modular monolith cannot. A modular monolith is one deployable unit with enforced internal boundaries; microservices split those modules into separately released services. Pay for the split when separate teams must ship on separate schedules.

Deployment independence is the main thing microservices buy that a modular monolith cannot. A modular monolith is one deployable unit with enforced internal boundaries; microservices split those modules into separately released services connected over the network. Pay for the split when separate teams must ship on separate schedules, and not before.

This article is about the decision rather than the technology. We cover what each architecture actually gives you, the operational bill that arrives with a distributed system, the five questions that settle the choice in most organisations, the case for running both, and the uncomfortable part every architecture post skips: what it costs to go back.

What each one is, without the folklore

A modular monolith is a single application, deployed as one artefact, whose internal structure is divided into modules with explicit, enforced boundaries. Modules talk through published interfaces rather than reaching into each other's tables. The enforcement is the point: without it you have a monolith, not a modular one, and the distinction is invisible on an architecture diagram but obvious in a pull request. One database, one deployment pipeline, one set of logs, in-process calls that cannot fail halfway.

Microservices are the same modules, each deployed independently, each usually owning its own data, communicating over HTTP or a message broker. What you gain is autonomy: a team can release, scale and roll back its service without coordinating with anyone. What you take on is a distributed system, with everything that implies: partial failure, network latency, eventual consistency, distributed tracing, versioned contracts between services, and a deployment surface that no longer fits on one screen.

Neither is modern or legacy. Plenty of very large products run as modular monoliths and plenty of small teams run twelve services badly. The variable is organisational, not technical, and a good architecture review starts by counting teams rather than by drawing boxes. The question is not which architecture is better but which set of problems your organisation is currently equipped to carry, because both choices trade one kind of complexity for another rather than removing it.

Microservices vs a modular monolith, side by side

DimensionModular monolithMicroservices
Unit of releaseOne artefact, one pipelinePer service, on each team's own schedule
Team fitOne to three teams sharing a codebaseFour or more teams needing autonomy
DataOne database; transactions are cheap and honestData per service; distributed consistency is your problem
Refactoring across boundariesA compiler-assisted renameA coordinated release across repositories
Failure modeThe whole app goes down togetherPartial degradation, plus cascades if you skip timeouts
ScalingScale the whole app, which is often cheaper than it soundsScale the hot service only
Local developmentClone, run, workContainers, stubs or a shared environment
Observability needLogs and metricsDistributed tracing is mandatory, not optional
Time to first releaseFastestSlower; platform work precedes product work

When microservices are clearly the right call

Four conditions make the overhead worth paying, and they are conditions about the organisation as much as the software.

  • Multiple teams blocked on one another. When three teams queue behind a single release train and a rollback for one feature reverts everyone's work, the coordination cost has exceeded the platform cost.
  • Genuinely different scaling profiles. A pricing engine that needs thirty instances at peak next to an admin console that needs one is a real argument for separation.
  • Different reliability or compliance boundaries. A payments component under stricter change control, or a component that must run in a specific region for data residency, benefits from being its own deployable.
  • Different runtimes for good reason. A machine learning service in Python beside a transactional core in Java is an honest split, provided the reason is the workload and not personal preference.

When a modular monolith is clearly the right call

For most products under most conditions, the monolith is the rational default, and saying so is not conservatism. With one or two teams, the coordination cost microservices remove barely exists, while the cost they add lands on day one. You get transactions instead of sagas, a stack trace instead of a trace correlation exercise, and refactoring across module boundaries that your compiler helps with rather than fights. Crucially, you keep the option to split later, because module boundaries drawn in code are the same boundaries you would eventually deploy separately.

The monolith is also the better answer when the domain is not yet stable. Boundaries drawn before you understand the business are drawn in the wrong place, and moving a boundary inside one codebase takes an afternoon, while moving it between two services takes a quarter. Getting the modules right first is the substance of multi-tenant SaaS architecture: the decisions that avoid a rewrite.

The hybrid almost everyone ends up with

The realistic end state for a growing product is not one or the other. It is a modular monolith carrying most of the domain, with two or three services extracted for specific reasons: the machine learning inference service, the document processing worker, the notification fan-out, the integration adapter with an unreliable third party. Each extraction is justified individually and on record. This is a coherent architecture, not a compromise, and it is what we build most often.

The pattern for getting there without a rewrite is the strangler pattern: route traffic through a facade, move one capability at a time behind it, and keep both paths live until the new one is proven. For a shell-and-modules variant at the front end, see super app architecture.

Five questions that settle it

Ask these in order and stop at the first honest no.

  • How many teams need to deploy without asking permission? Fewer than three, and the case is weak.
  • Is the domain stable enough that you would defend these boundaries in a year?
  • Who runs the platform: tracing, service templates, CI per service, on-call rotas? If the answer is nobody, that is your first project.
  • Can you name a component whose scaling or compliance profile genuinely differs from the rest?
  • What breaks if service A is down and service B is not, and have you written that behaviour down?
  • Does your delivery process already survive a bad release, with feature flags, staged rollout and a rehearsed rollback? Distributed systems punish teams who cannot do this in one artefact.

What it costs to change your mind

The two directions are not symmetric, and this is the single most useful thing to know before choosing. Splitting a well-modularised monolith is ordinary work: extract the module behind its existing interface, give it a database, add retries and timeouts, and cut traffic over. We would typically scope one such extraction at four to eight weeks depending on data ownership. Splitting a monolith with no internal boundaries is a different project entirely, because you have to discover the boundaries first, usually under a characterisation test harness.

Merging services back is worse. Consolidating four services into one means reuniting four databases, reconciling data that has drifted apart, and unpicking asynchronous flows that are now load-bearing. We have seen that take longer than the original split. The asymmetry is the argument: start with the option that is cheap to leave. An API layer in front of the existing system, described in adding an API layer to a legacy monolith, keeps that option open at low cost.

Where both choices fail

Microservices fail when they are adopted for prestige or hiring rather than autonomy: a four-person team running nine services spends its week on pipelines, and each service is under-tested because nobody has time. They also fail when services share a database, which produces the coupling of a monolith with the latency of a distributed system. Modular monoliths fail when the modularity is aspirational: no enforcement in the build, so within a year every module imports every other module and you have a big ball of mud with folders. If your current system is already that, neither architecture is your next move; stabilising and documenting it is, and regenerating documentation for undocumented systems is often where we start.

What this looks like as an engagement

Architecture is decided inside a build, not sold separately. SaaS and cloud-native application development at Eazyware starts at $31,500 or ₹20,80,000 and runs to $126,000 or ₹84,00,000; larger multi-team platforms fall under product and platform development from $42,000 or ₹28,00,000. When the question is how to decompose an existing system rather than how to build a new one, a ten-day Sprint Zero at $3,250 or ₹2,00,000, credited to the build, produces the module map and the extraction order. All starting prices are on the pricing page, and our other decision guides sit on the comparison hub. Our university ERP modernisation is the clearest example of boundaries being extracted one at a time instead of in a rewrite.

Martin Fowler's MonolithFirst argues from observed cases that teams who started with microservices on a new product mostly regretted it, and that successful microservice systems tended to begin as monoliths that were later split. It is the most useful primary source on this decision. On our side, the strangler pattern covers how to move without stopping the business.

Draw the boundaries first and deploy them separately only when a team, a scaling profile or a regulator gives you a reason.

Frequently asked questions

Is a modular monolith just a monolith with better folders?

▾

Only if the boundaries are unenforced. A modular monolith uses build-level rules, module systems or package visibility so that one module cannot reach into another's internals or tables. Without enforcement the structure decays within a year. The enforcement, not the folder layout, is what preserves the option to split later.

How many teams do you need before microservices make sense?

▾

As a rule of thumb, three or more teams that are genuinely blocked by a shared release train. Below that, the coordination cost microservices remove is small while the platform cost they add is immediate: tracing, per-service pipelines, contract versioning and on-call. Count teams and release conflicts, not services.

Can we start with microservices and merge later if we are wrong?

▾

You can, but it is the expensive direction. Merging means reuniting separate databases, reconciling drifted data and unpicking asynchronous flows that have become load-bearing, which often takes longer than the original split. Splitting a well-modularised monolith is the cheaper move, so start with the option that is easier to leave.