System design

Modular Monolith or Microservices? Decide Before You Split

Choose service boundaries using deployment needs, ownership, consistency, and operating costs. A worked example shows when a separate worker is enough.

On this page

This is a worked architecture example with invented product constraints, not a description of a client system.

A service boundary buys independent deployment and operation. It also introduces a network call, another failure mode, and a new place where data can arrive late. The question is whether a specific boundary is worth those costs now.

Imagine a SaaS team of five engineers. Its product has accounts, a customer catalog, orders, reporting, and email notifications. Traffic is modest. Reports occasionally consume enough CPU to slow the API. The team is considering turning each noun into a service.

Name the problem the split should solve

“Reporting needs to stop slowing the API” is a measurable problem. “We need microservices to scale” leaves too many decisions hidden.

For the example, set a narrower objective: interactive requests must remain responsive while reports run, report concurrency must be limited, and a failed report must be retryable. That does not yet require independently owned databases or five service repositories.

A separate worker process built from the same codebase may solve the immediate resource-isolation problem. It can run on its own instances and consume a bounded job queue while the API remains a single deployment. That is already a meaningful operational boundary.

Establish modules before distributing them

In the application, assign ownership: orders owns order state, the catalog owns product definitions, and reporting reads an explicit projection or a documented query interface. A module boundary is enforceable even when modules share a process.

Avoid arbitrary cross-module table updates. Keep dependency direction visible in imports and tests. If every order change requires editing reporting, accounts, and notifications, separate deployments will not remove that coordination. They will distribute it.

Martin Fowler’s Monolith First describes the advantage of discovering useful boundaries before paying the cost of distributed services. It is a useful starting argument, not proof that every new system should have the same shape.

Compare two concrete designs

DecisionModular application with a reporting workerIndependently deployed services
Release ownershipOne team coordinates API and worker releasesTeams can release within compatible contracts
Data changesLocal transactions and explicit module interfacesEach owner manages its data; cross-service work needs coordination
Reporting loadWorker scales independently of the API processReporting scales independently and consumes its own data interface
Failure handlingJob retries and worker recoveryJob recovery plus network failures, stale data, and contract mismatches
Operating workFewer deployment units, one shared codebaseMore deployments, access policies, dashboards, and on-call procedures

The worker option addresses the known bottleneck without committing the team to distributed ownership. Choose it for this example, then measure API latency, report completion time, queue age, and engineering effort to change a report.

Put consistency requirements in the decision record

A report that is accurate to the last minute has different requirements from a balance used to approve an order. Ask which decisions require a current, authoritative answer and which displays can tolerate a stated lag.

Suppose orders emits events for reporting. Writing the order and sending an event in separate steps can lose an event after a crash. The transactional outbox pattern makes that intent durable alongside the state change, while downstream consumers still handle duplicate deliveries. Include reconciliation so missed or malformed records can be detected and repaired.

A split is incomplete until the team can answer: who owns the truth, what is allowed to be stale, how do retries behave, and how do we recover when the two views disagree?

Define the evidence that would justify another boundary

For our hypothetical team, revisit a reporting service when one of these conditions becomes observable:

  • Reporting has its own team and release cadence, with recurring coordination delays.
  • Its storage model or access controls no longer fit the application’s database boundary.
  • Worker isolation no longer contains the measured load or failure behavior.
  • A stable reporting contract exists and the team can operate it independently.

These are review triggers, not automatic instructions to extract a service. Estimate the migration and ongoing operating costs against the actual problem. One noisy query may need a different index before it needs a new database owner.

A future extraction should also have a route back. Build the new read model from a reproducible source, compare results while the old path still serves users, move a controlled share of reads, and retain the old path until the comparison and recovery checks pass. Decide who owns writes throughout; casual dual writes make rollback harder.

Record a decision you can revisit

For the example, the decision record can be short:

Keep the product as a modular application. Move report execution to a separately scaled worker using a bounded queue. Orders remains authoritative. Report freshness and job recovery are explicit product behavior. Review extraction when independent ownership or measured isolation requirements justify it.

The outcome is not an architecture label. It is an understandable set of boundaries, operating responsibilities, and conditions under which the design should change.