lumo

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

  1. Distinguish code boundaries from independently deployed service boundaries.
  2. Trace how a local call changes when it becomes a network request.
  3. Choose an architecture using release needs, data ownership and failure behavior.
  4. Describe a safe path from a modular monolith to an independently operated service.

Builds on 2 lessons

Start with lesson 914 min
  1. Vertical and horizontal scaling: when to use each14 min
  2. Stateless and stateful services: impact on scaling18 min

Video 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.

Check yourselfAnswer in your head first, then reveal
Kartly runs three copies of the same application behind a load balancer. Has it become a microservices system?Show answerHide answer
No. Those are replicas of one application. Microservices separate capabilities into independently deployed services; adding copies changes capacity, not that architectural boundary.

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.

Modules separate responsibilities. The outer application boundary determines what Kartly releases together; its existing pricing dependency remains external.

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.

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.

A timeout describes what the caller observed. It does not prove that the remote service did nothing.

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:

  1. Kartly receives a product-page request.
  2. It requests suggestions with a product ID.
  3. The service computes a list of suggested IDs.
  4. The reply does not reach Kartly before its deadline.
  5. 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.

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

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

Check yourselfAnswer in your head first, then reveal
Kartly’s catalog and checkout have separate folders, but checkout imports catalog’s internal database model. What is the first improvement?Show answerHide answer
Introduce a supported catalog interface and remove the internal import. A network split is not the first fix: it would preserve the dependency while making it harder to change.
Check yourselfAnswer in your head first, then reveal
The suggestions service times out, but later records that it completed the lookup. Did Kartly’s timeout handling fail?Show answerHide answer
No. The deadline describes how long Kartly waits, not whether the service finishes. Kartly should still return the page without suggestions after that deadline.
Check yourselfAnswer in your head first, then reveal
Inventory reserves stock in its database, then checkout fails before saving the order in another database. Does rolling back checkout release the stock?Show answerHide answer
No. Checkout’s local rollback does not undo a committed reservation elsewhere. The workflow needs explicit recovery or coordination, which is a reason to keep these writes together initially.
Check yourselfAnswer in your head first, then reveal
Suggestions now deploys separately, but every release requires a simultaneous Kartly release. What should the team investigate?Show answerHide answer
Investigate the contract and the boundary. Separate deployment jobs do not provide useful independence when callers must change in step.

Summary

  1. Keep boundaries inside a modular monolith until independent deployment or operation has a demonstrated benefit.
  2. A monolith can have clear modules and multiple replicas. Microservices separate deployable capabilities, not merely files.
  3. Moving a call across a network adds uncertainty. Define deadlines, retries and user-visible fallbacks.
  4. Data ownership and compatible contracts determine independence. Cross-service writes require explicit coordination or recovery.
  5. Extract one justified capability, measure the result and keep a rollback path. More services are not the goal.

Report a mistake

Tell us what’s wrong in this lesson and we’ll fix it.