Write a decision record for a modular monolith and a credible extraction trigger.
Early architecture should make the next useful change affordable. A small team has limited time for deployments, incident response, and coordination. Multiple services can isolate ownership and scaling, but they also create network boundaries, partial failures, version contracts, and additional operations. Choose those costs when the problem supports them.
A monolith can still have clear modules. Billing, reporting, and account access can expose narrow interfaces while sharing one deployment. The useful boundary hides a business decision or integration detail. It does not merely split files by technical noun. A reporting module can own export creation and result retrieval while the rest of the application does not know its storage layout.
Martin Fowler's monolith-first discussion presents an experience-based argument for starting simpler and learning boundaries. It is not a theorem that every startup should use one deployment. A team with an existing service platform or a workload requiring strong isolation may reasonably choose otherwise. State the conditions that make the selected architecture appropriate.
Preserve extraction options through contracts and ownership, not speculative infrastructure. Avoid direct writes to another module's internal tables when an owned operation can express the intent. Record dependencies and business invariants. If a later service split needs asynchronous delivery, a durable event boundary can support it, but adding a queue to every function call before there is a reason increases work.
Worked example
A fictional two-engineer team builds a reporting product for ten pilot customers. Peak load is five exports per minute. It chooses one web application, one database, and one separately runnable worker process from the same codebase. The reporting module owns job state and result metadata. The account module owns permission checks. The worker can scale independently as a process without requiring a separate product service contract.
The decision record states a review trigger: sustained export work interferes with interactive latency despite bounded concurrency, or separate teams need independent release ownership. It does not use a customer count alone as a trigger. A hundred light customers can be easier than one heavy customer.
Record the boundary before the deployment
A modular boundary defines who owns a decision and what callers may rely on. A deployment boundary defines where code runs and how it fails. They can align, but they need not. In the reporting example, the web and worker processes can share code and a database while exposing explicit owned operations.
code
1Reporting interface2createExport(actor, input, operationKey) -> jobId3getExport(actor, jobId) -> authorized status4requestDownload(actor, jobId) -> permitted result access56Reporting owns7job state, input version, attempt authority, result pointer89Callers must not10write job.state directly or construct raw storage paths
This interface hides storage naming and state transitions from the page. It also states that authority is an input to protected operations, not an assumption held by the caller. The exact implementation may be a function call today and an API later. The contract should not promise that extraction will be free; network failure, versioning, and distributed transactions will still require work.
Compare two concrete options
Concern
Shared codebase with separate worker process
Independently deployed reporting service
CPU-heavy jobs
Scale worker processes separately
Scale service workers separately
Release coordination
One coordinated release contract
Versioned cross-service contract
Failure boundary
Process separation, shared dependencies remain
Network boundary, shared dependencies may still remain
Data ownership
Must enforce module discipline
Must define storage and event ownership
Operational cost
Fewer deployments to maintain
More health, auth, retries, and compatibility work
This comparison prevents a false choice between one undifferentiated process and many services. A separate worker can remove CPU work from request handling without immediately introducing an independent product service. Conversely, sharing one database can still couple performance and failure even after code is split into services.
At five exports per minute, average arrival is one every twelve seconds. If each export uses six worker-seconds, average offered work is thirty worker-seconds per minute, about half one continuously available worker's time. This is an invented average-load estimate, not a capacity guarantee. Bursts, long jobs, retries, and memory requirements can still require concurrency limits and measurement.
Write a useful extraction trigger
A trigger should connect evidence to a decision. For example: export jobs exceed the interactive latency budget during representative bursts even after concurrency is bounded and the worker process is separated. The next investigation tests whether the shared database, CPU host, or storage dependency causes the interference. Only then choose the boundary that addresses it.
If the shared database is the bottleneck, moving worker code to a service while retaining the same expensive queries may not solve the problem. If a distinct team needs independent releases, versioned interfaces may be justified even without high load. The trigger depends on the cost being paid, not on a round customer count.
A good decision record includes a review date or observation condition, owner, reversibility, and rejected alternative. Reversibility is partial. Changing an internal interface can be cheap; untangling data written directly by many modules can be expensive. Preserve change options by restricting those writes now.
Preserve business consistency
A report may need billing data to decide eligibility. It should obtain a defined billing fact or call an owned permission operation instead of reaching into arbitrary billing tables. The returned fact has a meaning and freshness requirement. A cached plan name may be sufficient for a label but insufficient for enforcing a current paid entitlement.
If two modules must commit a shared invariant together, an in-process transaction can sometimes simplify the first design. Splitting them across services may require a new consistency protocol and compensation rules. Do not propose a queue as a universal substitute for atomic business decisions. Events communicate committed facts or durable intentions under an explicit contract.
Misconceptions and a second exercise
One misconception is that folders automatically enforce modularity. If any caller writes any table, ownership remains informal. Another is that service extraction automatically removes shared failures. A separate deployment can still depend on the same database, credentials, or overloaded provider.
Exercise: exports are slow because each job scans the same large billing table. The proposed fix moves the worker to a new service but keeps the query and database. Identify the missing reasoning. The change may isolate process resources, but it does not establish relief for the shared query bottleneck. Inspect query workload and ownership, test a bounded query or appropriate read model, and measure interactive impact. Award one point for identifying the unchanged dependency, one for preserving billing semantics, one for a targeted experiment, and one for an explicit extraction condition.
In an interview, choose the smallest architecture that meets the actual constraints and name the evidence that would make you change it. Avoid both an unconditional microservice plan and an unconditional monolith rule. The decision is credible because its limits are visible.
Exercise and solution
A teammate proposes six microservices so the product can scale later. Compare that with the supplied workload. The model answer asks which component needs independent scaling or isolation now, counts the new operational boundaries, and prefers the smaller design unless a concrete requirement emerges. Award one point per element. An answer that rejects services in every circumstance is also weak because it ignores the possibility of a real isolation need.
Interview probe and wrap-up
How do you prevent a monolith from becoming difficult to change? A strong answer discusses module ownership, dependency direction, tests around business behavior, and explicit integration contracts. Follow up with a reporting query that needs billing data. A weak answer relies only on folders named clean architecture. A small deployment is valuable when its internal responsibilities remain understandable and its future change triggers are explicit.
An export service moves to a separate process. A controlled peak-load test shows its expensive database query still consumes the same shared database capacity, and interactive latency remains above the target. Which conclusion follows?
AThe process split has not resolved the measured shared-database bottleneck; investigate the query and resource contention before adding more export processes.BSeparate deployment establishes database isolation, so the remaining latency must come from the browser.CIndependent worker scaling will remove the bottleneck even if each worker issues the same query against the same saturated database.DThe split proves the product needs separate databases immediately, without checking query cost or consistency requirements.
APut every function behind a queue now.BChoose service names before learning responsibilities.CUse owned operations and explicit data/consistency contracts.DLet each module update all tables directly.
AA new service template becomes available.BMeasured isolation or release-ownership cost that the current boundary cannot address economically.CA preference for independent repositories.DA round number of accounts alone.
Can you distinguish module ownership, process isolation, and service deployment, then identify a measured reason to change the boundary? State the relevant identifiers, failure boundary, and evidence in your own words before selecting your confidence.