100%

Lesson 3 of 3

Keeping Extraction Cheap Without Paying For It Now

A modular monolith should make extracting a module a decision you can take later at moderate cost — without adopting distributed-system costs today for a service you may never need.

9 min read

The strongest argument for a modular monolith is optionality: you keep the option to extract a module without buying it today. That argument only holds if you actually do the small number of things that make extraction cheap — otherwise "we can always split it later" is a sentence, not a plan.

The five things that keep the option open

  1. 1

    Enforce the module surface in the build

    Everything else depends on this. If forty files import a module's internals, extraction begins with forty edits and an argument about each. The rule costs an afternoon to add and prevents the entire class of problem.

  2. 2

    Give each module its own schema and forbid cross-module joins

    Data coupling is invisible to import checks and is the thing that actually blocks extraction, as the previous lesson's scenario shows. Published views and events are the sanctioned crossings.

  3. 3

    Make cross-module calls asynchronous where the business allows it

    A synchronous call becomes a network call with partial failure after extraction. An event that already goes through an outbox becomes a message on a queue with almost no change to either side. Choose events wherever the caller does not need the result to proceed.

  4. 4

    Keep module interfaces coarse and business-shaped

    Ten fine-grained methods become ten network round trips. One method that expresses a business operation becomes one call. This is the same lesson as chatty API design, applied before there is a network to be chatty over.

  5. 5

    Do not share domain types across module surfaces

    If billing's surface returns the catalogue's `Course` entity, extraction means serialising a type two modules share. Publish simple data structures that the owning module controls, exactly as the clean-architecture module argued for ring boundaries.

JustificationEvidence you should be able to showVerdict
Independent scalingOne module's resource profile differs by an order of magnitude, and scaling the whole app is measurably wastefulStrong
Release independenceTeams are blocked on each other's releases, with counted incidents per quarterStrong
Different availability requirementOne module must stay up when others are down, or must not be able to take them downStrong
Different technology genuinely requiredA workload needs a runtime the monolith cannot host — a specialised library or hardwareStrong
Team autonomy in the abstractNo counted blocking incidents; teams simply want their own thingWeak — enforce module ownership first
The codebase is largeSize alone, with no named painWeak — size is not a problem, coupling is
Modernisation or hiring appealNothing measurable about the systemWeak — and expensive
What actually justifies extracting a module, and what does not.

In practice

One extraction taken, one declined

The health-tech platform from the first lesson reached forty engineers across seven teams three years later. Two extraction proposals arrived in the same quarter: notifications, and clinical records.

Constraints

  • Notifications: bursty load, 20x peak during appointment reminders, no user waiting on it
  • Clinical records: steady load, strictest audit requirements, transactionally involved in most flows
  • Seven teams, with release blocking now measured at six incidents per quarter
  • Platform team of four with solid operational tooling

Decision

Extract notifications. Decline clinical records and instead split it into two enforced sub-modules within the monolith.

Why

Notifications met three of the strong criteria: an order-of-magnitude different load profile, no caller waiting on its result, and it already communicated only through outbox events — so extraction changed the transport and nothing else. Clinical records met none: its load was ordinary, and it participated in transactions with scheduling and billing that would have become distributed transactions with compensations, trading a hard audit guarantee for eventual consistency in the most regulated part of the system.

What it cost

The notifications extraction took five weeks and added a queue, a second deployment pipeline and a new on-call surface. Reminder bursts stopped consuming capacity that the scheduling path needed, which was the measurable win. Declining the clinical-records split disappointed the team that owned it, and the architect wrote down what would change the answer: if audit requirements moved to an append-only event log, the transactional coupling would loosen and the decision should be revisited.

A team proposes extracting their module

As a developer

Sees a well-bounded module, a team that owns it, and a clean surface — everything the guidance says makes a good service. Supports the proposal on the grounds that the boundary is ready.

As an architect

Agrees the boundary is ready and treats readiness as a precondition rather than a reason. Asks what the extraction buys that the module boundary does not already provide: which scaling, release or availability property changes, and what number will show the improvement. If nothing measurable changes, the right answer is to keep the boundary and spend the five weeks elsewhere — and to say so plainly, because "we could" is not "we should".

Your modular monolith is well enforced, and a team argues that extracting their module would let them deploy without waiting for the weekly release train. Is that a strong justification?Reveal

It is pointing at a real cost, but probably at the wrong solution. The pain is a weekly release train, and the cheapest fix is usually to release more often — trunk-based development, feature flags, and a pipeline fast enough to deploy several times a day. That improves matters for every team at once, whereas extraction improves it for one team and adds a service to operate. Extraction becomes the right answer when releasing more frequently is genuinely blocked: when one module's changes require a lengthy migration or a regulated validation step that others should not have to wait for, or when the modules must run at different versions for a business reason. Ask what specifically prevents deploying twice a day before accepting release independence as the justification.

Key takeaways

  • Optionality is only real if you enforce surfaces, own data per module, prefer events, keep interfaces coarse and avoid shared domain types.
  • Those five steps improve the monolith on their own terms; adopting distribution costs in advance does not.
  • Strong extraction justifications are different scaling, release blocking, different availability, or a genuinely different runtime.
  • Weak justifications are codebase size, abstract autonomy and modernisation.
  • Before accepting release independence as a reason, check whether releasing more often would fix it for everyone.