| Author: Abdullah Ahmed | Category: Software Consulting
A development team extracts the customer module from a monolith. Within weeks, the new service needs synchronous calls back to orders, billing, permissions, and reporting. Releases still require coordination, but failures now cross a network.
The deployment boundary changed before the business boundary was understood. Splitting code can be mechanically straightforward while separating responsibility remains difficult. Before choosing a service, determine which decisions it owns, which data it controls, and what other parts of the business can expect from it.
A monolith can remain a sensible home for a well-structured application. Service extraction becomes useful when a particular boundary offers a concrete benefit, such as independent change, a distinct operating requirement, or clearer ownership that the team is ready to support.
Begin with the pain behind the proposal
Ask what splitting the application is expected to improve. Slow builds, unclear responsibilities, poor database queries, and conflicting product priorities are different problems. Some respond to modularisation or delivery improvements without requiring distributed services.
Collect examples of the current friction. Which changes repeatedly require several teams? Which workloads need different capacity? Which failures affect unrelated capabilities? Which releases are delayed because nobody knows the impact of a shared change?
Turn the evidence into a testable objective. “Allow the fulfilment team to change dispatch rules without a billing release” is more useful than “adopt microservices.” It also provides a way to reject an extraction that adds infrastructure but preserves the same coordination.
Record constraints alongside the objective. A small team with limited operational cover may benefit more from clear modules and reliable deployment than from a collection of independently hosted components. Architecture has to fit the people who will operate it.
Map capabilities through real work
Choose several business journeys and follow the decisions involved. An order journey might include quotation, acceptance, stock reservation, dispatch, invoicing, and support. These are candidate capabilities, not automatically separate services.
Invite domain experts to explain the rules and exceptions. Developers see dependencies in code; operations staff see dependencies in commitments. A dispatch decision may require a payment condition or a customer-specific restriction that the code structure does not make obvious.
Write down the commands people issue and the facts they care about. “Reserve stock,” “confirm dispatch,” and “issue invoice” reveal responsibilities more clearly than a list of database tables. Identify where a decision is final and where another capability merely needs to be informed.
Keep the initial map simple enough to discuss. The goal is to expose disagreements about ownership, not to produce a polished diagram that conceals them. Mark uncertain boundaries and return to concrete cases when participants interpret a term differently.
Listen for words with different meanings
The word customer may mean a sales prospect, a billed legal entity, a delivery contact, or a support account. Forcing all those meanings into one universal service can create a large shared model that every team must negotiate.
Martin Fowler's explanation of bounded contexts describes boundaries within which a domain model remains internally consistent, with explicit relationships between contexts. This helps distinguish shared identifiers from genuinely shared business meaning.
In an illustrative distribution business, sales may own the relationship and opportunity history, while billing owns invoice recipient details and payment terms. Both refer to the same organisation, but they need not expose identical models or update rules.
Document translations between contexts. If “active customer” means different things in sales and billing, the interface should state which meaning it carries. Ambiguous shared vocabulary can become an integration defect when code turns a conversational assumption into a hard rule.
Locate the decisions that must stay consistent
A boundary is difficult when one operation must immediately update several proposed services while preserving a strict business invariant. Identify those invariants before distributing the writes.
For example, a reservation operation may need to ensure that allocated units do not exceed available stock under the chosen inventory rules. Keeping the relevant decision and state together can be simpler than coordinating multiple services for every reservation.
Ask which delays the business can tolerate. A reporting view may lag behind an operational update; a permission decision may require current information. Eventual consistency is a business behaviour that needs agreement, not just a technical label.
If separating a capability requires compensation, describe the compensation in business terms. Releasing a reservation may be possible; undoing a dispatched parcel is a different operation. Do not assume every step has a clean inverse.
Examine change patterns in the existing code
Use source history to see which parts change together. Repeated co-change can suggest a shared responsibility, although it may also reflect poor modularity or a broad release process. Treat the evidence as a clue to investigate.
Inspect runtime call paths and data access. A module that appears isolated in a directory tree may depend on shared helpers, database triggers, or another team's tables. Identify those dependencies before promising independent deployment.
Look for accidental coupling that can be removed in place. Passing a small value object instead of a whole application entity, introducing a module interface, or replacing a hidden query can clarify the boundary without adding network failure modes.
Do this work on representative changes. If the proposed service can support a real feature through a narrow interface while leaving unrelated modules untouched, you have stronger evidence than a static dependency diagram alone provides.
Assign data ownership explicitly
For every important fact, name the capability allowed to change it. Other capabilities may hold projections or cached copies, but they should know where authoritative updates originate and how corrections arrive.
Shared database access often undermines a service boundary. If several applications update the same tables directly, their schemas and business rules remain coupled. Moving code to a new repository does not resolve that dependency.
Plan how readers obtain the information they need. Options include a supported query interface, a purpose-built read model, or events that update a local projection. Choose according to freshness, volume, and failure tolerance rather than adopting one mechanism everywhere.
Define deletion and correction behaviour too. A projection that receives creates and updates but never removals can accumulate stale or inappropriate information. Ownership includes the lifecycle of copied data, not only its initial publication.
Design an interface around a useful promise
An interface should express what the capability can do and what callers can rely on. A reservation service might accept a request to reserve items and return a reservation result with an explicit expiry. Exposing every underlying table field would make callers depend on internal design.
Separate queries from commands in the contract. State whether a command is completed immediately or accepted for later processing. Include how callers discover the final outcome and how repeated requests are handled.
Specify failure behaviour in ordinary language. If inventory is unavailable, may an order remain pending? How long may it wait? Who resolves the exception? These answers shape timeouts, queues, and user experience more directly than an API naming convention.
Version changes with consumers in mind. Additive fields are not automatically harmless if clients reject unknown values or infer behaviour from an enum. Establish contract tests and a communication process for changes that alter meaning.
Evaluate the cost of crossing the boundary
A local method call and a network request have different operating characteristics. Remote calls can time out, return uncertain outcomes, or fail while other services remain healthy. Each boundary introduces work around authentication, retries, tracing, and deployment.
Draw the call path for a common user action. If it crosses proposed services repeatedly to retrieve small pieces of state, reconsider the boundary or provide a more useful operation. Chatty communication can be a symptom of separating data that belongs with one decision.
Estimate the support burden. Each independently deployed service needs an owner, delivery pipeline, configuration management, monitoring, and recovery procedures. Shared platform tooling can reduce repetition, but someone still owns the business behaviour.
Compare that cost against the original objective. If the extraction does not reduce coordination, isolate a meaningful failure, or meet a distinct operating need, a modular monolith may offer a better trade-off.
Prove the boundary inside the monolith
Introduce a module interface and route callers through it before moving the implementation. Stop direct access to its internal data and make dependencies explicit. This creates a lower-cost way to discover missing operations.
Try changing an internal rule without changing consumers. Then try a representative consumer change. If both require widespread edits, investigate whether the contract is too narrow, too detailed, or based on the wrong responsibility.
Use tests to protect business behaviour at the boundary. Focus on rules, permissions, and failure outcomes rather than duplicating every implementation detail. These tests can later support extraction by checking that the new implementation honours the same promise.
Enforce the module boundary through code review or available architecture checks. Without enforcement, convenience imports and direct queries can gradually recreate the coupling while the diagram remains unchanged.
Select a first extraction with a learning objective
The smallest module is not always the best first service. A trivial component may avoid the data, deployment, and recovery questions that matter. Conversely, starting with the most central transactional capability can create excessive migration risk.
| Question | Useful evidence |
|---|---|
| Does it own a coherent decision? | A documented business responsibility and its invariants |
| Can it change independently? | A representative change completed through the module contract |
| Can it own its data? | A plan to remove uncontrolled external writes |
| Can the team operate it? | Named support ownership and rehearsed recovery |
| Will extraction solve the original pain? | A measurable delivery or operating improvement |
Write an extraction decision record that includes rejected alternatives and unresolved assumptions. Set a review point after the first capability moves. The purpose is to learn whether the architectural direction produces the expected benefit before repeating it.
Plan coexistence before moving production traffic
The old and new implementations may need to coexist while callers migrate. Decide which owns writes at each phase, how data is reconciled, and what rollback means after the new service accepts changes.
A routing layer can move selected operations gradually, but it must not obscure ownership. Maintain a clear inventory of callers and retire old access paths when their replacements are proven. Permanent compatibility layers can otherwise become another monolith of integration logic.
Observe business outcomes during the transition. A technically healthy service can still reject valid operations or create stale projections. Compare representative transactions and keep a way for staff to identify which implementation handled them.
Finish by removing obsolete dependencies. If the extracted service still relies on shared deployment steps, unrestricted database access, and emergency edits by another team, the boundary remains incomplete.
Compare two possible boundaries in an order system
Suppose a team proposes a service for every major noun: customers, orders, products, and invoices. This produces a tidy list, but it may force almost every business operation to coordinate several owners. An invoice decision needs more than a row from each table; it needs a coherent view of billable work and the rules for issuing it.
An alternative starts from responsibilities. The ordering capability owns acceptance of a commercial request. Fulfilment owns dispatch progress. Billing owns invoice issuance and adjustments. Each may keep the limited data it needs to fulfil its promise, with explicit references to other capabilities.
This alternative is still a hypothesis. Test it with exceptions: a partial shipment, a cancellation after dispatch, an amended customer agreement, and a corrected invoice. Ask who decides each outcome and which information must be current. The difficult cases reveal whether the proposed owners can act without circular permission requests.
If billing cannot issue any invoice without several synchronous reads from the monolith, investigate why. It may need a purpose-built billable-work event or projection. It may also be premature to separate billing while its rules are still intertwined with order acceptance.
Do not force the answer to be a service. Keeping billing as a protected module can establish data and rule ownership while the team learns. Deployment independence can follow when it offers a demonstrated benefit and the integration contract has matured.
Document what a boundary does not own
A short list of exclusions can prevent gradual responsibility drift. A dispatch capability might own shipment state but not customer credit policy. It may consume a clearance decision from the responsible owner without recreating that owner's rules.
Review new feature requests against those exclusions. If several legitimate requests repeatedly cross the same line, revisit the model. Boundaries should be stable enough to support independent work and revisable when business evidence shows they are wrong.
Align team responsibilities with the proposed service
Independent deployment is difficult when a team can change code but cannot modify configuration, manage data, or respond to incidents. Clarify the authority and support expected of the owning team.
Agree on how consumers request changes and report problems. A service contract includes human coordination: compatibility notices, support expectations, and ownership of cross-service incidents. Without these agreements, technical independence can leave users caught between teams.
Review whether the team has enough capacity to maintain both the extracted service and its migration path. Temporary adapters and data synchronisation are real products during coexistence. Assign their retirement work rather than assuming it will happen after the exciting part is finished.
After extraction, compare actual change lead time and incident handling with the original objective. If coordination remains high, use the evidence to improve the contract or reconsider the boundary. Repeating an extraction pattern without reviewing its result can distribute the same problems across more infrastructure.
Make the next decision smaller
Before approving a broad decomposition programme, choose one candidate capability and write down its decisions, data ownership, consumers, and failure behaviour. Attempt a meaningful change through an internal module boundary.
If that exercise exposes contradictions, resolve them while the code is still together. If it demonstrates a useful independent responsibility and the team can support the operating cost, extraction becomes a justified next step. The goal is a system that is easier to change and run, with boundaries that reflect how the business actually works.