Module 01 · Lesson 11 of 14Free
Monoliths and microservices: choosing an architecture
- Level
- Intermediate
- Topic
- System design fundamentals
- Reading time
- 18 min
When you finish this page you can
- Distinguish code boundaries from independently deployed service boundaries.
- Trace how a local call changes when it becomes a network request.
- Choose an architecture using release needs, data ownership and failure behavior.
- Describe a safe path from a modular monolith to an independently operated service.
Builds on 2 lessons
Start with lesson 914 minVideo lesson · coming soon
handles about 50 requests a second on its single app server. A change to product suggestions is ready, but a checkout bug blocks the release.
“Should suggestions become a separate service?” asks the team. The question sounds like a code organization problem, but the answer changes how requests fail.
Which boundaries should stay inside Kartly, and which deserve their own deployment?
The traffic and server match Kartly’s current course stage. The blocked release and product-suggestion feature are this lesson’s illustrative scenario.
Kartly runs three copies of the same application behind a load balancer. Has it become a microservices system?Show answer
A release boundary is different
A packages its application capabilities into one deployable unit. A keeps that unit but gives its internal parts explicit interfaces and ownership. One deployment does not require tangled code. Shopify described choosing this approach in its 2019 engineering account.1
For this lesson, picture Kartly with catalog, checkout and suggestions modules:
Diagram not drawn yet: kartly-modular-release-boundary.svg
A browser calls one Kartly application containing catalog, checkout and suggestions modules. All three modules share one release boundary and use one PostgreSQL database. The application also calls the existing pricing service.
A architecture separates business capabilities into autonomous services. Each owns its data and exposes an interface. Independent deployment is a defining benefit; a small source file or a separate repository is not enough.3
Consider two proposed Kartly designs:
| Design | What changes together? | How checkout reaches catalog |
|---|---|---|
| Modules inside one application | One application release | A function call |
| Independently deployed services | Each service can have its own release | A network request |
Neither row tells you how many machines exist. Use vertical and horizontal scaling to reason about separately.
The opening release problem is real. But before moving code across a network, Kartly must ask whether its internal boundaries work at all.
Separate responsibilities before running services
In our initial implementation, the suggestions module reads catalog tables directly. Checkout also edits those tables to update stock. A catalog change can therefore break callers that its owner never intended to support.
That is : one part depends on another part’s details. In this scenario, renaming a catalog column requires finding every caller.
Our proposed modular version changes those dependencies:
| Module | Interface it exposes | Data it owns |
|---|---|---|
| Catalog | Read product details; reserve stock | Products and stock |
| Checkout | Create and read orders | Orders and order lines |
| Suggestions | Return suggested product IDs | Suggestion rules and results |
These are contracts, not necessarily . Initially, they are ordinary function interfaces inside Kartly.
Suppose checkout needs a product’s current title and price. Catalog returns a value containing those fields. Checkout does not receive a mutable catalog object or permission to edit catalog tables.
For this lesson’s design, checkout copies the agreed title and price into the order line. That snapshot belongs to the order. A later catalog rename must not rewrite a customer’s purchase history.
Make ownership enforceable
For our proposed Kartly implementation:
- Each module has a small public entry point.
- Callers import that entry point, not internal files.
- Automated import checks reject forbidden dependencies.
- Module owners review changes to public contracts.
- Queries that access another module’s tables move behind its interface.
These rules make the boundary more than a folder name. Without enforcement, the next urgent feature can restore the old shortcuts.
Shopify’s account describes defining and respecting component boundaries without splitting the application into services. That is evidence for the approach, not a claim that every monolith will remain manageable indefinitely.1
Kartly now has a concrete alternative to extraction. It can improve ownership without adding another runtime dependency.
Choose the cheapest useful boundary
Should Kartly split catalog, checkout and suggestions into services now?
Keep the application unchanged.
The team avoids immediate restructuring. But direct table access remains, so each catalog change still needs a broad search and coordinated edits.
Build a modular monolith first.
The team introduces explicit module interfaces and ownership. Releases remain shared, but changing internal catalog details no longer requires changing every caller.
Extract suggestions immediately.
Suggestions could release separately if its contract stays compatible. Kartly also gains network failures, another deployment and additional operating work.
Why this choice
For this scenario, the only observed release collision does not yet justify another service. First, make the suggestions boundary explicit and record how often unrelated changes actually block delivery.
The modular option addresses our known dependency problem. It does not solve independent releases, and we should not pretend it does.
The choice changes if a stable suggestions module repeatedly needs releases that checkout delays. A named team must also be ready to operate it. At that point, independence has a recurring benefit rather than an assumed one.
This reasoning follows Fowler’s warning that services add their own development and operating costs. His recommendation is to preserve modularity without distributing a system before its complexity justifies that cost.2
Keep the decision testable. In our scenario, record blocked-release time, the reason for each block and the modules changed together.
If most “suggestions changes” also modify checkout, the boundary is not ready for independent deployment. If they rarely overlap, extraction becomes more plausible.
A better code boundary gives Kartly options. It does not yet reveal the most important cost of using those options.
A remote call has uncertainty
Imagine a later experiment: Kartly moves suggestions into a separately deployed service. This is a hypothetical extension, not a change to Kartly’s current course stage.
Inside the application, the caller receives a return value or a local error. Across a network, a adds another possibility: the caller does not know whether the remote work completed.
Distributed services introduce communication, and failure-handling costs. Long chains of calls can also add .3
Diagram not drawn yet: kartly-suggestion-timeout.svg
A top-to-bottom sequence shows Kartly requesting suggestions, the suggestions service computing a result, and the reply failing to arrive. Kartly reaches its deadline and returns the product page without suggestions.
For our extraction experiment, set a 100 ms deadline for suggestions. The page’s core product information must not wait longer for this optional feature.
Trace one request:
- Kartly receives a product-page request.
- It requests suggestions with a product ID.
- The service computes a list of suggested IDs.
- The reply does not reach Kartly before its deadline.
- Kartly returns the product page without the suggestions section.
The user can still inspect the product. The missing section is preferable to a frozen page.
That behavior is a design choice, not an automatic property of services. If Kartly waits indefinitely, the separate service still blocks the page.
A means one part fails while another remains operational. In this experiment, the suggestions service might be healthy even though communication with Kartly fails.
Retries depend on the operation
Our suggestion lookup has no write effect, so repeating it cannot create another order. Still, we choose no immediate retry on the page path: another attempt would spend the remaining deadline and add work during trouble.
Now imagine moving order creation across the same boundary. Repeating an uncertain request could create another order. That operation needs idempotency, not a general “retry every timeout” rule.
The network boundary therefore changes the contract. Kartly must define deadlines, fallback behavior and retry rules alongside request and response fields.
Data ownership determines real independence
A separate suggestions process is not independent if it reaches into catalog’s tables. Catalog cannot safely rename a column until every deployed reader changes.
Microsoft’s guidance connects service autonomy with private data ownership and stable interfaces. Shared schemas and rigid protocols create dependencies across service boundaries.3
Apply that principle to our experiment:
| Information | Owner | What another component may do |
|---|---|---|
| Current product details | Catalog module | Request a supported product view |
| Suggestion result | Suggestions service | Request suggested product IDs |
| Purchased title and price | Checkout module | Read the stored order snapshot |
Suppose suggestions returns IDs for products that were removed moments earlier. Kartly’s page assembly checks those IDs through catalog and omits missing products. Our scenario tolerates fewer suggestions; it does not tolerate inventing product details.
This keeps the service contract narrow. Suggestions selects IDs. Catalog decides whether those products currently exist.
Keep one atomic change together
Stock reservation creates a harder boundary.
For our initial design, catalog and checkout can participate in one PostgreSQL transaction. Either stock reservation and order creation commit together, or neither does. Module ownership does not require separate databases.
If those writes move into separately owned databases, an ordinary transaction on either database cannot cover both. Distributed coordination would need an additional protocol.
Imagine inventory reserves the last unit, then order creation fails. Someone must release that reservation or finish the order later. The customer must also see an honest intermediate result.
For Kartly, that recovery work is a reason to keep these tightly related writes together initially. Later, sagas examine workflows with explicit recovery across services.
A promising extraction boundary minimizes both shared releases and shared atomic changes.
Independence must repay operating costs
Return to the hypothetical suggestions service. Assume the team now has repeated evidence that its releases are blocked.
Independent deployment and scaling are potential service benefits. Fault isolation depends on callers handling failures correctly; process separation alone does not provide it.3
Before extraction, the team writes down this operating contract:
| Component | Responsibility and state | If it fails |
|---|---|---|
| Kartly app | Assemble the page; own the | The page fails; app detect it |
| Suggestions service | Return IDs; keep replaceable suggestion results | Kartly omits suggestions; timeout counters rise |
| PostgreSQL | Keep catalog and order records | Core reads or writes fail; suggestions cannot repair them |
| Existing pricing service | Return pricing information | Follow the existing pricing failure policy; extraction does not change it |
This table exposes an important limit: extracting suggestions does not remove Kartly’s database or pricing dependencies.
For the service itself, we require a health check, an owner, a compatible rollback and measurements of errors and timeouts. Recovery means replacing a failed instance or reverting a bad release, then checking that callers receive valid results again.
Use numbers without inventing savings
The following is an illustrative capacity calculation. Assume suggestions consumes 1 ms of CPU time per product-page request. Assume every request in the festival peak is such a page.
Calculate peak suggestion work
1,000 requests/s × 0.001 CPU-s/request = 1 CPU-second/s
Suggestions accounts for about one core of work under these assumptions.
Compare with Kartly’s server
1 core ÷ 8 cores = 12.5% of CPU capacity
Moving suggestions might move this work elsewhere. It does not remove the other work or prove that extraction saves money.
We have no measured service capacity or hosting price here. Therefore, this estimate supports investigation, not a machine count or a savings claim.
If suggestions now needs independent releases, how should Kartly extract it?
Keep it as a module and improve the release process.
The interface remains local and failures stay simpler. The team still accepts a shared release schedule.
Extract one optional capability with an owner.
The team moves only suggestions behind its existing narrow contract. Product pages have a defined fallback, and success can be measured before further splits.
Split catalog, inventory, checkout and suggestions together.
The team changes many boundaries at once. A page failure becomes harder to attribute, and stock-and-order coordination needs a new design.
Why this choice
Under the changed assumptions, suggestions has a demonstrated need for release independence. It also has a tolerable failure mode: the page can omit it.
First deploy the new service and verify its contract. Then route a small share of suggestion lookups to it. Compare errors, page latency and result validity before increasing that share.
Keep the old implementation available during the experiment. Reverting routing restores it without moving order data.
The choice changes if the contract still changes with every caller release, or nobody can operate the service. Then improving the shared release process remains the safer option.
Release freedom needs compatible contracts
In our experiment, the service first returns a list of product IDs. Later, its team wants to return scores too.
They can preserve the existing ID field and add optional scores. Old callers continue using IDs. Removing IDs immediately would force Kartly and the service to deploy together.
A service boundary has failed its purpose if every change requires a synchronized release. Independent deployment requires compatible contracts, not just separate build jobs.3
For Kartly, make the extraction decision from evidence:
- How much blocked-release time disappeared?
- Did page latency or timeout frequency increase?
- How often did a service change still require a caller change?
- How much operating work did the new service introduce?
If release coordination remains unchanged, another service has not solved the original problem.
The earlier stateless and stateful services lesson explains what must survive instance replacement. Here, the additional question is who can change that state and its contract.
Where this shows up
- Design: a notification service: delivery is a distinct capability with its own failure and recovery behavior.
- Design: an e-commerce checkout: order, inventory and payment boundaries determine coordination work.
Kartly’s next question is whether the independence gained is worth the infrastructure and operating cost.
Common mistakes
Equating a monolith with one machine. A monolith describes a deployment boundary. Replicating it changes capacity, not its division into capabilities.
Calling folders independent services. In our design, a module becomes a service only when its runtime and deployment boundary move. A folder rename does neither.
Sharing tables after claiming independence. Kartly’s proposed service contract hides catalog’s schema. Direct table access would undo that separation.
Assuming a separate process protects users. In our timeout scenario, protection comes from the deadline and fallback. Without them, the page still waits.
These mistakes all confuse a visible boundary with the behavior that boundary must support.
Check your understanding
Kartly’s catalog and checkout have separate folders, but checkout imports catalog’s internal database model. What is the first improvement?Show answer
The suggestions service times out, but later records that it completed the lookup. Did Kartly’s timeout handling fail?Show answer
Inventory reserves stock in its database, then checkout fails before saving the order in another database. Does rolling back checkout release the stock?Show answer
Suggestions now deploys separately, but every release requires a simultaneous Kartly release. What should the team investigate?Show answer
Summary
- Keep boundaries inside a modular monolith until independent deployment or operation has a demonstrated benefit.
- A monolith can have clear modules and multiple replicas. Microservices separate deployable capabilities, not merely files.
- Moving a call across a network adds uncertainty. Define deadlines, retries and user-visible fallbacks.
- Data ownership and compatible contracts determine independence. Cross-service writes require explicit coordination or recovery.
- Extract one justified capability, measure the result and keep a rollback path. More services are not the goal.