Lesson 2 of 4 · 60 min

Choose a small architecture that can change

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

ConcernShared codebase with separate worker processIndependently deployed reporting service
CPU-heavy jobsScale worker processes separatelyScale service workers separately
Release coordinationOne coordinated release contractVersioned cross-service contract
Failure boundaryProcess separation, shared dependencies remainNetwork boundary, shared dependencies may still remain
Data ownershipMust enforce module disciplineMust define storage and event ownership
Operational costFewer deployments to maintainMore 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.

Sources

docsMartin Fowler on starting with a monolithmartinfowler.comdocsAWS transactional outbox patterndocs.aws.amazon.comdocsPostgreSQL constraintspostgresql.org

Checkpoint

A separate worker process shares one database with the web app. What is isolated?

AEvery release contract.BEvery database failure.CAll data ownership automatically.DProcess execution can be separated; shared database coupling remains.
Sign up free to answer and see why

Checkpoint

Five jobs/minute each need six worker-seconds. Average offered work?

ASix workers continuously.BThirty worker-seconds/minute, before retries and bursts.CFive worker-seconds/minute.DThree hundred worker-seconds/minute.
Sign up free to answer and see why

Checkpoint

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.
Sign up free to answer and see why

Checkpoint

Which practice preserves an extraction option?

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.
Sign up free to answer and see why

Checkpoint

What is a credible extraction trigger?

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.
Sign up free to answer and see why

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.

Not yetGetting thereConfident

Sources

Free to read · better with Enzo

Learn it with Enzo

Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.