skip to content

A modular monolith has clean, enforced module boundaries. What specifically makes extracting one of its modules into a standalone microservice straightforward, and what commonly makes it hard in practice even when the code-level boundary looks perfect?

level: seniorimportance: must knowfreq 62%

answer

  1. API/events already abstract the call - extraction swaps implementation
  2. shared DB/schema is the hardest hidden coupling
  3. local ACID transaction becomes distributed/saga after extraction
  4. sync in-process call becomes network call with real failure modes
  5. give module its own schema before extracting, to surface coupling early

basics

~20 s

If a module only talks to others through its public API/events and doesn't share its database tables, you can swap that in-process API for a network call with far less risk - but if it secretly shares tables or needs instant answers from another module, extraction breaks things until you fix that first.

solid answer

~50 s

Extraction is straightforward exactly to the degree the module's coupling was already accounted for: if `billing` only ever called `orders` through `OrdersApi`/DTOs or reacted to `OrderPlaced` events, extracting `orders` means replacing that in-process implementation with an HTTP/gRPC client and swapping the in-process event publisher for a message broker - call sites in `billing` barely change. What commonly makes it hard even with a clean code boundary is everything the code-level check couldn't see: a shared database schema where `orders` and `billing` tables are joined directly or share foreign keys, synchronous request chains where extraction turns in-process microsecond calls into network calls with real latency and failure modes, transactions that used to be one local ACID transaction across two 'modules' now needing a saga or eventual consistency, and operational maturity - service discovery, per-service CI/CD, monitoring - the team may not have built yet. In practice the database split is usually the single hardest, highest-risk part of the whole extraction.

go deeper

for a junior

Should have a basic sense that pulling a module out into its own service means it now talks over the network instead of in-process, and that this is a bigger change than moving code around.

for a middle

Should be able to name the mechanical steps of extraction - swap API implementation, swap event publisher for a broker, split the database - even without deep detail on the harder parts.

for a senior

Should identify the database and transactional/latency issues as the real risk even when the code boundary is clean, and describe pragmatic mitigations like giving the module its own schema ahead of time.

for a principal

Should reason about when extraction is actually worth doing at all - independent scaling, team ownership, compliance - versus modules that should stay in the monolith indefinitely, and weigh operational/organizational readiness alongside the technical mechanics.

## What extraction actually involves The promise of a modular monolith is that, because module boundaries are already enforced at the code level, extracting a module into its own deployable service later should be a mechanical, low-risk operation rather than a rewrite. Concretely, extraction means three things happen roughly in parallel for the module being pulled out: 1. Its public API interface gets a new implementation that makes an HTTP or gRPC call to the newly standalone service instead of an in-process method call. 2. Any domain events it published or consumed switch from an in-process publisher to a message broker so publishers and consumers can run in separate processes. 3. The module's own database tables move to a database the new service owns exclusively. If the module's boundary was genuinely respected the whole time — no other module ever imported its internal repository or entity classes, and no other module's queries ever joined against its tables — then callers in the remaining monolith don't need to change at all beyond the API implementation swap, because they were already coded against an interface and DTOs, not against the module's internals. ## The first obstacle: the database That 'if' is exactly where extraction usually gets hard in practice, and the gap between a clean code-level boundary and a clean extraction is the most important thing to understand about this path. **The single biggest risk is the database.** - Module-boundary tools check Java/Kotlin class dependencies; they have no visibility into SQL, so two modules can pass every boundary check while one module's query joins directly against another module's tables, or a foreign key constraint spans both modules' schemas. - None of that shows up until extraction time, when it turns into a hard requirement to either duplicate data with a synchronization mechanism to keep it eventually consistent, or rewrite the query as a cross-service call — both real engineering work, not a configuration change. Teams that anticipate extraction usually get ahead of this early by giving each module its own schema, even while still sharing one physical database instance, and treating cross-schema joins as a boundary violation on par with a cross-module import, which surfaces the coupling far earlier and far more cheaply than discovering it during the actual split. ## The second obstacle: transactions and latency The second common obstacle is transactional and latency behavior changing shape. Inside the monolith, a use case that touches two modules, say placing an order and reserving inventory, can often run inside one local, ACID database transaction, and a synchronous in-process call from `orders` to `inventory` costs microseconds. Once `inventory` is extracted, that same interaction becomes a network call with real latency, the possibility of partial failure where the network call times out after inventory was actually reserved, and no more shared transaction — correctness now requires either a distributed transaction pattern like a saga with compensating actions, or accepting eventual consistency and redesigning the use case around it. Code that was written against a synchronous API and never had to think about 'what if this call fails halfway' now has to. ## The third obstacle: operational readiness The third obstacle is organizational and operational, not code-level at all: does the team have, or need to build, service discovery, a per-service CI/CD pipeline, independent monitoring/alerting, and on-call practices for a new deployable? Extracting one module without that operational maturity often means the 'microservice' is deployed and monitored as an afterthought, a worse outcome than leaving it in the monolith. ## A pragmatic sequencing A pragmatic, commonly recommended sequencing is therefore: 1. **First**, make the module boundary real, with a public API and events verified in CI. 2. **Second**, give the module its own schema even before extraction, forcing all cross-module data access through the API/events and catching hidden database coupling early. 3. **Third**, watch the module's actual traffic and team ownership to see if it genuinely needs independent scaling or independent deployment cadence, since not every clean module needs to become a service, and modules with low traffic or that change in lockstep with the rest of the system are often better left inside the monolith indefinitely. 4. **Only then extract**, starting with the module that has the clearest, already-schema-isolated boundary and the strongest business case, such as needing to scale independently, or a separate team wanting an independent release cadence.

  • Why is giving a module its own database schema before extraction considered such an effective early step?
    Because it turns hidden, invisible database coupling into an immediate, visible failure the moment someone tries a cross-schema join or foreign key - the same way a public-API rule turns hidden code coupling into a build failure - so the team discovers and fixes the coupling months or years before the actual extraction, when it's cheap, instead of during the extraction itself, when it's expensive and time-pressured.
  • What changes about error handling when a synchronous in-process call between two modules becomes a network call after extraction?
    An in-process call either returns or throws essentially instantly and can't 'partially succeed' in an ambiguous way; a network call can time out after the remote side actually completed the work, can fail transiently and need a retry, and can leave the caller unsure whether the operation happened, so code has to add idempotency keys, retries with backoff, and often circuit breakers, none of which the in-process version needed.
  • Is a clean module boundary sufficient justification on its own to extract a module into a microservice?
    No - a clean boundary is what makes extraction low-risk if you decide to do it, but the decision itself should be driven by a real need such as independent scaling, an independent team wanting its own release cadence, or a hard isolation requirement, not by the boundary simply being clean; many well-bounded modules are best left inside the monolith indefinitely because splitting them would only add operational cost with no corresponding benefit.

Like a company spinning off a subsidiary that already has its own P&L, its own staff, and its own filing cabinet - the paperwork to make it a separate legal entity is easy; a department that still shares payroll systems and a single ledger with the parent company is the one that takes months to actually separate.

saying these in an interview costs you the question

  • assumes a clean code-level module boundary automatically means extraction is easy, with no mention of the shared database risk
  • can't explain how a local transaction across two modules has to change after extraction
  • treats extraction as a pure refactor with no operational/organizational cost
  • believes every well-bounded module should eventually become a microservice
  • has no answer for what happens when the new network call between the split services fails

context