Software architecture

Modular monolith or microservices: test the boundary

Choose the independence a system actually needs: capacity, deployment, data ownership or operational responsibility. This proposed reporting design compares a module, a worker process and an independently owned service.

Provisional choice
Separate export execution capacity while core acceptance and access remain together
Evidence needed
Measured constraint, data owner, compatibility and operational ownership
Artifact
Boundary worksheet and comparison; no measured scaling or cost claim
Architectural model with blue rooms on a shared foundation and a separate amber workshop connected by one bridge.
Conceptual illustration of a deliberate extraction boundary and the connections it retains.

Name the independence you need

A SaaS team wants report generation to scale without adding more web servers. That requirement justifies separating execution capacity. It does not yet justify giving reporting its own database, API versioning policy and on-call responsibility.

Begin by naming the independence the change must buy. Is the team trying to deploy a business capability without coordinating every release? Contain a failing workload? Assign a different owner? Scale a CPU-heavy worker? These are different architectural jobs. Several can be addressed inside a modular application or by running another process from the same codebase.

For the fictional reporting product used here, the first proposal is a separately scalable worker that consumes an explicit export-job contract while the core application retains job acceptance and tenant authorization. Service extraction becomes a later option if independent ownership and release requirements emerge. The worksheet records what evidence would change that decision.

Put the transaction boundary on the diagram

Our example contains workspace administration, report definitions and export execution. Accepting an export requires a valid tenant context and a committed job record. Rendering the file can happen later. This gives the team an execution boundary without requiring every participant in request acceptance to become a network service.

Responsibility and consistency in the fictional reporting product
ResponsibilityProposed initial ownerConsistency requirement
Workspace membership and permissionsCore applicationEvaluate current access at protected operations
Report definition and accepted input referenceCore applicationBind the job to the intended definition or snapshot
Job acceptance and visible statusCore applicationCommit accepted work before acknowledging it
File renderingExport worker processConsume a defined job input and report a terminal outcome
Result accessCore applicationCheck current access before exposing the file
Core application owns membership, report definitions, job acceptance and result access. A separately scalable worker consumes job input and reports outcomes; generated files are stored separately.
Figure 1. Proposed execution split for the fictional reporting product. The job contract is explicit while the core retains acceptance and access ownership. View full-size figure.

If the renderer reads arbitrary core tables, calls internal helper functions through an unrestricted package dependency and writes status fields directly, the proposed boundary is mostly decorative. It may still provide useful CPU separation, but the team has not established an independently evolvable service contract. Write the permitted interface first. An export job should identify the tenant, the input version or snapshot and the requested output. The completion interface should identify the job and its outcome. Decide how schema changes remain compatible with queued work. A job created by yesterday's application can be consumed by today's worker, so even a shared repository needs a compatibility policy at that boundary.

Pay for the new failure modes explicitly

A function call becoming a remote call changes its failure behavior. The caller can time out while the callee continues. A response can be lost after a mutation succeeds. A rolling deployment can put different contract versions on either side. The extraction proposal must show where those cases are handled.

Martin Fowler's Microservice Premium describes the additional deployment, monitoring and distributed-data work that accompanies service separation. His Monolith First argues for learning useful boundaries before making them expensive to move, while acknowledging that this route also requires modular discipline. These are architectural arguments, not a benchmark establishing a universal team-size threshold.

For the reporting example, a worker crash should leave a job inspectable and recoverable under a defined ownership policy. A storage outage should produce an observable processing outcome without disabling workspace administration. A new report-definition field should not require an emergency synchronized release just to keep old queued jobs readable. Each statement needs a concrete implementation and a check against the actual deployment process.

The asynchronous job contract provides a worked starting point for status and cancellation semantics. The deadline calculation covers time accounting for remote calls. Extracting a service does not remove those responsibilities from the product team.

Compare three candidate shapes

A module boundary, a process boundary and a service boundary are not interchangeable. The terminology varies across organizations, so the table defines the practical distinctions used in this decision.

Practical distinctions between module, process and service boundaries
ShapeWhat can change independentlyObligations that remain
Module in one application processInternal implementation behind a controlled interfaceShared process resources and a coordinated application release
Separate worker process with shared product ownershipWorker capacity and, if packaging permits, worker rolloutJob compatibility, ownership recovery and explicit shared-data rules
Independently owned serviceIts contract implementation, release and resources within compatibility rulesNetwork failures, service operations, data ownership and consumer migration
A module separates implementation, a worker process separates execution capacity and a service adds independent contract and operations ownership. Each option retains specific compatibility obligations.
Figure 2. Candidate boundary shapes. These are qualitative responsibilities, not scores or a mandatory migration ladder. View full-size figure.

The middle option is useful when the immediate problem is execution capacity. It can keep one product team responsible for the whole workflow while allowing worker concurrency to change. It still needs deployment and failure testing. A process launched separately is not automatically operationally independent.

The third option becomes more persuasive when reporting has a stable contract, a distinct owner and a release cadence that repeatedly conflicts with the core application's. It becomes less persuasive when ordinary feature work still requires coordinated changes to both databases and both APIs.

Splitting repositories does not make that coordination disappear.

Fill the boundary worksheet with evidence

Download the service-boundary worksheet and decision comparison. They contain proposed questions and the reporting example's provisional decision. Empty evidence fields are intentional: there are no invented incident counts, deployment frequencies or cost estimates behind this recommendation.

Review the candidate using these questions:

  • Which measured constraint does separation remove, and how will the team detect that improvement?
  • Which records have one authoritative owner after the change, and which cross-boundary transaction must be redesigned?
  • Can the new unit deploy while existing producers and consumers remain compatible?
  • What happens when its request, acknowledgment or completion notification is lost?
  • Who owns its alerts, restoration procedure and consumer support?

Treat an unanswered ownership question as a design gap rather than assigning a reassuring score. A candidate with an independent deployment pipeline but shared, uncontrolled writes to the same business records still has a coupled data boundary. Conversely, a module with a clear owner and strict interfaces may already deliver much of the organizational clarity the team needs.

Keep workload evidence separate from architectural judgment. Queue age, worker utilization and web-request latency can establish whether rendering competes with interactive work. They cannot by themselves prove that a separate database or a new organizational team is necessary.

Make extraction a reversible sequence

First constrain the module's dependencies and introduce the explicit job interface. Then run the worker as a separately controlled process and check compatibility with queued work. Measure whether the original capacity or release constraint improves. Only after that evidence exists should the team decide whether separate service ownership and storage solve an additional problem.

Data separation requires its own migration plan. Shared-table access cannot be removed merely by changing an import statement. Define the authoritative owner, migrate consumers and verify that old write paths no longer operate before retiring them. The expand-contract migration walkthrough explains how compatibility can survive a staged schema change.

Revisit the provisional decision if report execution acquires a distinct business lifecycle, if multiple products need the same stable capability or if a shared release repeatedly blocks independently owned work. Keep the evidence with the decision record. The reason to extract is an independence requirement the team can demonstrate, not the number of services on the architecture diagram.

Sources

Documentation checked .

  1. Martin Fowler: Monolith First
  2. Martin Fowler: Microservice Premium