A system begins with one primary deployable application.
APPLICATION
│
├── catalog
├── basket
├── inventory
├── ordering
└── payment
Its modules share one release lifecycle and commonly run in the same process, although the deployment may also include worker processes or scheduled jobs.
The components can call one another directly. When they share a database and transaction manager, related changes can participate in one local transaction. The application can be built, tested and released as one unit.
Then somebody draws a new diagram.
CATALOG SERVICE
BASKET SERVICE
INVENTORY SERVICE
ORDER SERVICE
PAYMENT SERVICE
The application has become a collection of services.
The boxes are smaller.
The arrows are more numerous.
The architecture diagram has acquired the stately confidence of a regional rail map.
But what has actually improved?
Perhaps catalog can now be deployed independently.
Perhaps inventory can scale separately.
Perhaps one team can own payment without coordinating every release with the rest of the application.
Those can be significant benefits.
The new design has also replaced ordinary in-process calls with remote interactions.
BEFORE
basket code
│
▼
catalog function
AFTER
basket service
│
▼
network
│
▼
catalog service
The function call has gained:
- serialisation;
- network latency;
- service discovery;
- authentication;
- timeouts;
- retries;
- partial failure;
- versioned contracts;
- remote observability.
The network boundary is not simply a tidier line between two responsibilities.
It is a new operating environment.
That leads to the question I want to use throughout this article:
What valuable boundary am I buying, and is it worth the distributed-systems cost?
A network boundary must buy something
A monolith is primarily a deployment boundary
The word monolith is often used as though it were a diagnosis.
It can conjure an application that is large, old, fragile, poorly tested and difficult to change.
But those are not required properties of a monolith.
A monolithic application is broadly one primary deployment and change boundary.
MONOLITHIC APPLICATION
│
├── user interface
├── application logic
├── domain modules
├── persistence access
└── supporting workers
It may run as one process, several application instances, or a small collection of workers released together. It may be small or large, well structured or tangled, modern or old, frequently deployed or rarely changed, and owned by one team or several.
A monolith can be badly designed.
A collection of services can also be badly designed, only with more network sockets.
Architecture quality, process topology and deployment topology are related but different properties.
Look for a concrete requirement
Suppose I move catalog into a separate process but still require:
- the same release train
- the same database
- the same deployment window
- the same team approval
- the same scaling policy
- the same shared internal library
I have introduced remote communication without gaining much autonomy.
DISTRIBUTED DEPLOYMENT
but
COORDINATED CHANGE
That is a poor exchange.
A useful network boundary should normally support at least one concrete requirement such as:
- catalog must scale independently
- payment requires a stricter security boundary
- inventory has a separate availability requirement
- one team needs independent release control
- a capability must be reusable by several applications
- failure must be isolated from the main process
- the component requires a distinct runtime or resource profile
Without such a requirement, the network may be charging rent for an empty room.
A practical comparison
| Concern | Modular monolith | Independent services |
|---|---|---|
| Deployment | One application artifact | Several independently deployable artifacts |
| Calls | In-process | Remote |
| Typical latency | Lower | Higher and more variable |
| Failure model | Process-level failure | Partial and network failure |
| Transactions | Easier across modules sharing one database | Usually local to each service |
| Cross-domain queries | Local joins may be available | Composition or derived views |
| Scaling | Whole application commonly scales together | Services may scale independently |
| Contract evolution | Often one code change | Must support version overlap |
| Debugging | One process and call stack | Correlation and tracing required |
| Security boundaries | Primarily module and database controls | Separate workload identities and network policy |
| Operational surface | Smaller | Larger |
| Team autonomy | Possible, but one release unit can constrain it | Stronger when ownership is genuine |
| Technology choice | Usually shared runtime | Can vary, with additional support cost |
| Local development | Usually simpler | Requires dependency strategy |
| Best fit | Cohesive system with shared lifecycle | Capabilities needing distinct lifecycles |
The table does not select an architecture.
It shows where the cost moves.
Strong boundaries without distribution
One application can contain several real boundaries
Suppose the application contains these modules:
application
│
├── catalog
├── basket
├── inventory
└── order
Each module can expose a narrow application interface.
BASKET MODULE
│
│ catalog interface
▼
CATALOG MODULE
The catalog module can own:
- product rules
- product model
- catalog tables
- catalog migrations
- supported operations
The basket module does not need to query catalog tables directly.
It uses an explicit interface:
type ProductReader interface {
GetPurchasableProduct(
ctx context.Context,
productID ProductID,
) (PurchasableProduct, error)
}
The concrete implementation may still be an in-process call.
basket
│
▼
Go interface
│
▼
catalog implementation
That is a meaningful application boundary even though no network lies between the modules.
The important properties are:
- catalog controls its model
- basket depends on a supported contract
- internal catalog details remain private
- cross-module access is deliberate
This is often called a modular monolith.
It combines one deployment unit with explicit internal boundaries.
A modular monolith keeps local simplicity
An in-process module call retains several local properties.
The caller and callee usually share:
- one process lifetime
- one memory space
- one deployment
- one local call stack
- one runtime environment
The call may look like:
product, err := catalog.GetProduct(ctx, productID)
The function receives in-memory values.
It returns in-memory values.
The compiler can check much of the interface.
A debugger can follow the call directly.
A process crash affects both modules because they are part of one process.
A deployment replaces both modules because they are part of one artifact.
Those are real constraints.
They are also operational simplicity.
A modular monolith should not be treated as a temporary embarrassment awaiting enough containers to become respectable.
For many systems, it may be the appropriate architecture.
Not every boundary needs a service
Suppose two modules:
- change together
- share one invariant
- are owned by one small team
- scale together
- fail together acceptably
- have no distinct security requirement
Keeping them in one process may be the simpler design.
A module interface can still protect their internal responsibilities.
MODULE A
│
▼
EXPLICIT INTERFACE
│
▼
MODULE B
The network is not the only way to create discipline.
Repository rules, package visibility, database permissions, tests and code review can also preserve boundaries.
The question is whether the boundary needs independent runtime behaviour.
If not, a module may be enough.
What changes when a call becomes remote
A service creates a remote runtime boundary
Extracting catalog into a service creates a remote runtime boundary.
APPLICATION RUNTIME
│
└── basket module
CATALOG SERVICE
│
├── catalog runtime
└── one or more instances
The basket can no longer invoke catalog code directly.
It must send a request using a remote contract.
BASKET
│
│ GetProduct request
▼
NETWORK
│
▼
CATALOG
│
│ GetProduct response
▼
NETWORK
│
▼
BASKET
This boundary can permit:
- independent deployment;
- independent scaling;
- separate failure and restart lifecycles, subject to shared infrastructure and dependency coupling;
- a different technology where the benefit exceeds the support cost;
- a separate security policy;
- separate operational ownership.
Those are the reasons to introduce a service.
The network is not the purpose.
It is the mechanism through which the independence is enforced.
Compare:
product, err := catalog.GetProduct(ctx, productID)
with:
product, err := catalogClient.GetProduct(
ctx,
&catalogv1.GetProductRequest{
ProductId: productID.String(),
},
)
They look similar.
That resemblance is convenient.
It can conceal the larger path taken by the second call.
CALLER
│
├── validate local request
├── serialise request
├── resolve destination
├── acquire or reuse connection
├── authenticate peer
├── transmit bytes
│
▼
NETWORK
│
▼
SERVER
│
├── receive bytes
├── decode request
├── authenticate caller
├── authorise operation
├── run application logic
├── encode response
└── transmit response
Every stage consumes time and can fail.
A remote procedure call may be written like a local call.
It cannot be designed like one.
An in-process call may involve:
- function dispatch
- memory access
- local computation
A remote call adds:
- serialisation
- kernel networking
- network traversal
- proxy or load-balancer processing
- server scheduling
- deserialisation
- response traversal
The exact cost varies.
A call between two processes on the same machine is different from a call across availability zones.
A tiny request is different from a large streamed response.
A warm connection is different from a new secure connection.
The important point is not one universal latency number.
It is that a remote call normally costs more than an ordinary local call, and that remote latency has a distribution.
Most calls may complete quickly.
A smaller number may take much longer.
User-facing operations are often shaped by those slower calls rather than by the median alone.
Suppose checkout calls services sequentially:
CHECKOUT
│
▼
CATALOG
│
▼
INVENTORY
│
▼
PAYMENT
│
▼
SHIPPING
If each call consumes time, the total path accumulates that latency.
catalog latency
+
inventory latency
+
payment latency
+
shipping latency
+
checkout work
=
customer response time
The calls may be individually acceptable.
Together they may create a slow user experience.
Some independent calls can run concurrently:
CHECKOUT
│
┌───────────┴───────────┐
│ │
▼ ▼
CATALOG SHIPPING
│ │
└───────────┬───────────┘
▼
CONTINUE
Concurrency can shorten the critical path.
It does not remove the availability dependencies.
Checkout still needs both results before continuing.
An in-process component might offer:
- GetProductName
- GetProductPrice
- GetProductImages
- GetProductDimensions
- GetProductCategory
Calling five local methods may be harmless.
Calling five remote methods may require five network round trips.
BASKET ──► CATALOG: GetName
BASKET ──► CATALOG: GetPrice
BASKET ──► CATALOG: GetImages
BASKET ──► CATALOG: GetDimensions
BASKET ──► CATALOG: GetCategory
The service boundary should expose an interface shaped for remote use.
For example, GetPurchasableProduct might return the coherent information required for the caller’s operation.
That does not mean creating one enormous endpoint containing the whole database.
It means designing around useful remote interactions rather than copying every internal method onto the network.
A process boundary should influence API granularity.
Partial failure, deadlines and uncertain outcomes
Inside one process, a fatal process failure usually affects every module together.
Across services, one component can fail while the others continue running.
BASKET
running
CATALOG
unavailable
This is useful isolation.
It also creates states in which the application is only partly functional.
The basket may be able to load an existing basket but unable to validate a newly added product.
The storefront may show account information while product search fails.
The architecture needs to decide:
- Which functions remain available?
- Which calls should fail quickly?
- Can stale data be used?
- Should work be queued?
- What should the customer be told?
- How will the failed dependency recover?
Partial failure is not simply a smaller outage.
It is a requirement for intentional degraded behaviour.
A local call normally ends when:
- the function returns
- the process crashes
- the context is cancelled
A remote call may stop producing observable progress.
The caller cannot wait indefinitely.
request sent
│
▼
no response
│
▼
how long should caller wait?
The client needs a deadline or timeout.
That deadline should reflect:
- how long the result remains useful
- the complete caller deadline
- expected service latency
- remaining downstream work
- retry policy
A timeout is not merely a networking option.
It is part of the service contract.
The caller is saying:
After this point, this response is no longer useful enough for me to continue waiting.
Suppose the order service calls payment.
ORDER
│
│ AuthorisePayment
▼
PAYMENT
The order service reaches its deadline.
It knows that no successful result arrived in time.
It may not know:
- payment never received the request
- payment rejected the request
- payment authorised successfully
- payment is still processing
- the response was lost
The network boundary has introduced an ambiguous outcome.
That ambiguity requires:
- idempotency
- operation status
- reconciliation
- careful retry policy
- explicit pending states
The cost of the remote call is not only latency.
It is the possibility that one side changes state while the other side remains uncertain.
A client can retry after a selected failure.
attempt 1
│
└── timeout
attempt 2
│
└── success
The second attempt may overcome a transient problem.
It may also repeat a state-changing operation whose first outcome is unknown.
CreateOrder attempt 1
│
└── order 5001 created,
response lost
CreateOrder attempt 2
│
└── order 5002 created
Remote APIs need explicit retry semantics.
That can include:
- safe retrieval operations
- idempotent target-state updates
- stable operation identifiers
- deduplication records
- bounded backoff
- server retry guidance
A local method extraction did not require most of this.
The network boundary does.
A caller might think only about sending the request.
But a remote interaction has two journeys:
REQUEST
client ─────────► server
RESPONSE
client ◄───────── server
The request can succeed while the response fails.
The server can commit the operation while the caller reports an error.
This is why a client error does not always mean that the server did nothing.
The architecture needs business operation identity that survives the network attempt.
That identity may be carried through:
- idempotency key
- order ID
- payment attempt ID
- message ID
- workflow ID
Without it, the system cannot easily distinguish another attempt at the same work from genuinely new work.
Services create contract and data boundaries
Contracts must survive version overlap
Inside one codebase, a developer might change:
type Product struct {
ID string
Name string
Price int64
}
and update all callers in one commit.
Across independently deployed services, the producer and consumer may run different versions at the same time.
BASKET v3
│
▼
CATALOG v2
The contract must tolerate that overlap.
Changes need rules for:
- adding and removing fields;
- changing data types;
- changing field or status meaning;
- changing defaults and error interpretation;
- changing ordering or delivery guarantees;
- changing whether success means accepted or completed;
- introducing and retiring operations.
A machine-readable contract such as Protocol Buffers or OpenAPI helps define structure.
It does not decide compatibility policy.
The service owner must know which changes existing consumers can tolerate.
Suppose every service imports one shared package, company-domain-model.
It contains:
- Product
- Customer
- Order
- Payment
- Address
Updating the package may require every service to upgrade together.
shared package v17
│
├── catalog
├── basket
├── order
└── payment
The services have separate processes but one shared internal model.
The network boundaries do not create much conceptual independence.
A shared package can be useful for:
- generated contract code
- small technical utilities
- observability conventions
It becomes risky when it exports one universal business model that every service must adopt.
Service autonomy depends on limiting shared change, not merely creating separate repositories.
A service is independently deployable when its owner can release a compatible change without requiring simultaneous releases of every consumer and dependency.
That requires:
- backward-compatible contracts
- tolerant consumers
- safe database migrations
- independent build pipelines
- independent configuration
- independent rollback or roll-forward
- operational ownership
Suppose catalog adds a new response field.
Old basket instances should continue working.
Suppose catalog stops sending an old field immediately.
Any basket version that still requires it will fail.
Independent deployment therefore relies on an overlap period:
producer supports old and new contract
│
▼
consumers migrate
│
▼
old contract retired later
The architecture trades coordinated deployment for compatibility work.
That can be a good trade.
It is not free.
Private data makes autonomy real
Suppose catalog and basket are separate services but share tables.
CATALOG SERVICE ─────┐
├────► products table
BASKET SERVICE ──────┘
Basket can query catalog data directly.
A catalog schema change can break basket.
Catalog cannot evolve its persistence independently.
The process boundary exists.
The data boundary does not.
A stronger service boundary looks like:
BASKET SERVICE
│
│ catalog contract
▼
CATALOG SERVICE
│
▼
CATALOG DATABASE
Catalog owns:
- product meaning
- product rules
- catalog schema
- catalog migrations
- supported read and write contracts
Private ownership does not require a separate physical database server for every service. It can be enforced through separate credentials, schemas, databases or servers, provided other services cannot treat the owner’s persistence as their own interface.
This allows the service to change its internal storage without turning its table layout into a public interface.
It also removes easy local joins and cross-module transactions.
The independence is real because the cost is real.
Inside one monolithic database, checkout might execute:
BEGIN
│
├── create order
├── reduce inventory
├── create payment record
└── commit
If order, inventory and payment own separate databases, one ordinary local transaction cannot protect all three.
ORDER DATABASE
INVENTORY DATABASE
PAYMENT DATABASE
The workflow can reach intermediate states:
- order created, but inventory not reserved;
- inventory reserved, but payment declined;
- payment authorised, but the order response was lost.
The design now needs:
- workflow state
- retries
- idempotency
- compensation
- reconciliation
- event publication
- operational visibility
This is one of the most important costs of a network and data boundary.
A service does not merely move a function.
It can turn a local transaction into a distributed business process.
A monolithic database can perform:
SELECT
orders.order_id,
customers.name,
payments.status
FROM orders
JOIN customers
ON customers.customer_id = orders.customer_id
JOIN payments
ON payments.order_id = orders.order_id;
With service-owned data, those records live behind different owners.
Possible replacements include:
- API composition
- derived read model
- event-built projection
- reporting pipeline
- analytical warehouse
Each introduces trade-offs around:
- latency
- availability
- staleness
- storage duplication
- rebuild procedures
The service boundary creates autonomy by removing direct shared access.
It also removes conveniences provided by one local relational model.
A serious design acknowledges both sides of that exchange.
Autonomy, scaling and failure isolation
Scaling boundaries must match real workloads
Suppose product search receives ten times more traffic than checkout.
In one deployable application:
APPLICATION INSTANCES
│
├── catalog
├── basket
├── order
└── payment
Scaling the application adds capacity for every module, even if only catalog needs it.
A separate catalog service can scale independently:
CATALOG
│
├── instance 1
├── instance 2
├── instance 3
├── instance 4
└── instance 5
ORDER
│
├── instance 1
└── instance 2
That can improve resource efficiency.
It can also allow catalog to use a different:
- CPU allocation
- memory profile
- autoscaling policy
- cache design
- deployment frequency
Independent scaling is valuable when the workload difference is real.
If every service always scales together, the separate deployment units may not be buying much.
A monolithic application can also run several instances.
LOAD BALANCER
│
├──► application instance 1
├──► application instance 2
└──► application instance 3
That may be sufficient for many workloads.
The inefficiency appears when one capability dominates resource use or needs a different scaling model.
Before extracting a service for scaling, I should ask:
- Which component consumes the resource?
- Can expensive work move to a background queue or a separate worker pool?
- Can one module be optimised or given tighter resource limits?
- Can caching or database workload separation reduce the pressure?
- Can the monolith scale horizontally at acceptable cost?
- Does extraction create enough savings to repay its complexity?
“Microservices scale” is true in the same broad way that several lorries can carry furniture.
The useful question is whether this particular sofa required a fleet.
Process isolation does not guarantee capability isolation
In one process, an unrecovered fault in one module can terminate the entire application.
catalog defect
│
▼
application process crashes
│
▼
basket, order and payment disappear too
A separate catalog process can fail without terminating order.
- catalog unavailable
- order still running
That is valuable isolation.
But order may depend synchronously on catalog.
If so, order is alive but unable to complete important operations.
PROCESS IS HEALTHY
BUSINESS CAPABILITY IS NOT
Failure isolation requires more than process separation.
It may also require:
- optional dependencies
- cached or replicated read data
- degraded modes
- asynchronous handoff
- careful critical-path design
The process boundary contains some failures.
The dependency graph determines how far their effects travel.
This isolation is limited by shared dependencies. Separate processes may still fail together when they depend on the same database cluster, identity provider, network, region or deployment platform.
A service boundary isolates only the failures that the new runtime and dependency design genuinely separate.
Suppose catalog slows down.
Basket waits.
Checkout waits on basket.
The storefront waits on checkout.
CATALOG SLOW
│
▼
BASKET WAITS
│
▼
CHECKOUT WAITS
│
▼
CUSTOMER WAITS
Threads, connections and request slots accumulate.
Timeouts trigger retries.
Retry traffic increases catalog load.
One degraded service can create a cascading failure across otherwise healthy processes.
The network boundary can isolate process crashes while spreading latency and resource exhaustion through synchronous dependencies.
Resilience requires:
- deadlines
- bounded concurrency
- load shedding
- retry limits
- backoff
- circuit breaking
- capacity planning
Those mechanisms become part of the service architecture.
Independent deployment requires compatible change
Independent deployments reduce the size of each release.
They also create many possible version combinations.
- catalog v5
- basket v3
- inventory v7
- order v4
- payment v2
The system must work across supported combinations.
A rollout can temporarily produce:
- old caller → new server
- new caller → old server
Compatibility should exist in both directions where the rollout requires it.
Deployment strategy may involve:
- rolling updates
- canary releases
- feature flags
- backward-compatible schema changes
- traffic shifting
The smaller artifact reduces one blast radius.
The number of interactions increases the compatibility surface.
A service containing one capability can be changed without redeploying unrelated code.
A catalog release does not replace the payment process.
That can provide:
- smaller change sets
- clearer ownership
- faster rollback
- targeted scaling
- reduced unrelated failure exposure
This benefit becomes meaningful when the service is genuinely deployable and operable independently.
If every release still requires:
- the same approval board
- the same deployment window
- all services to use one branch
- all databases to migrate together
the organisation has retained coordinated delivery while multiplying artifacts.
The deployment boundary should match the change boundary.
Operating a service estate
Every service is an operational responsibility
One application deployment might require:
- one build pipeline
- one runtime configuration
- one deployment
- one set of health checks
- one log stream
- one alerting model
Ten services may require:
- ten buildable artifacts
- ten deployment definitions
- ten runtime identities
- ten configuration sets
- ten health models
- ten capacity profiles
- many dashboards and alerts
Shared platform tooling can reduce the repeated effort.
It cannot remove the need to understand each service.
Every service needs answers to:
- How is it built?
- How is it deployed?
- How is it configured?
- How does it authenticate?
- What are its dependencies?
- What does healthy mean?
- How is it scaled?
- How is it rolled back?
- Who responds when it fails?
A service is not only code with an HTTP port.
It is an operational responsibility.
A team can operate one or two services with bespoke scripts and manual knowledge.
As the service count grows, repeated operational work becomes significant.
Useful platform capabilities include:
- standard build pipelines
- deployment templates
- service identity
- secret delivery
- logging and metrics
- distributed tracing
- health conventions
- service discovery
- configuration management
- local development support
These provide a paved path for common service requirements.
Without them, every service team invents:
- its own deployment
- its own telemetry format
- its own retry policy
- its own certificate handling
- its own alerting
The cost of the network boundary then repeats manually across the organisation.
A microservice architecture is partly an application architecture and partly an operational platform commitment.
Observability must reconstruct the distributed call stack
Inside one process, one request may produce one local log sequence.
Across services:
- gateway log
- checkout log
- inventory log
- payment log
- database log
The timestamps may differ slightly.
Entries may arrive out of order.
One service may log a timeout while another logs success.
To reconstruct the interaction, the system needs:
- request IDs
- correlation IDs
- trace context
- structured fields
- central collection
- consistent clocks
Without these, debugging becomes:
- search five systems
- guess which entries belong together
- compare timestamps
- discover one service says “failed”
- while another says “committed”
The network boundary creates an observability boundary.
Tracing and correlation are not decorative production extras.
They are how the distributed call stack is reconstructed.
Each service can report:
- request rate
- error rate
- latency
- resource usage
It should also report dependency behaviour:
- outbound call rate
- dependency latency
- timeouts
- retry count
- circuit state
- connection-pool usage
A checkout service may appear slow because its own code is slow, because payment is slow, or because inventory retries are consuming the deadline.
The boundary needs measurements on both sides.
Otherwise every service reports:
I spent most of the request waiting politely for somebody else.
A service can be:
- running
- ready to receive traffic
- able to reach its database
- able to serve selected functions
- unable to reach one optional dependency
These are different conditions.
A health check used to restart a process should not fail merely because one downstream service is unavailable.
Restarting healthy callers can increase the incident.
A readiness check may remove an instance that cannot serve its intended traffic.
A business-level availability signal may report that checkout is degraded because payment is unavailable.
The network boundary creates several kinds of health:
PROCESS HEALTH
Can this process continue running?
INSTANCE READINESS
Should new traffic be sent here?
DEPENDENCY HEALTH
Can required remote operations complete?
CAPABILITY HEALTH
Can the customer journey succeed?
One green endpoint cannot summarise the entire graph.
Security and discovery become execution concerns
Inside one process, modules share one process identity and memory boundary.
Across services, every remote interaction needs decisions about:
- server identity
- client identity
- transport encryption
- authentication
- authorisation
- credential rotation
- certificate trust
- request provenance
The catalog service should know whether the caller is:
- the storefront
- the basket service
- an operator
- an unknown process
Authentication answers:
Who is calling?
Authorisation answers:
Is that caller allowed to perform this operation?
The network boundary can provide useful security separation.
It also creates more identities, credentials and policies to manage.
Separate services do not automatically create least privilege.
They create the opportunity and obligation to define it.
An in-process call knows where its target code lives.
A remote client needs to locate a reachable service endpoint.
logical service name
│
▼
service discovery
│
▼
one or more endpoints
│
▼
connection
The endpoint set may change because of:
- deployment
- autoscaling
- restart
- failover
- maintenance
The client or a load balancer must select an endpoint.
It may need to react when an endpoint becomes unhealthy.
This introduces:
- name resolution
- load balancing
- connection lifecycle
- endpoint caching
- health information
A local module call does not need a control plane to discover its neighbour.
A service call often does.
Testing and local development need deliberate seams
A modular monolith can test several modules together in one process.
test process
│
├── catalog
├── basket
└── order
A service system needs several kinds of evidence:
| Test level | Purpose |
|---|---|
| Unit | Exercise isolated business rules |
| Component | Exercise one service with controlled dependencies |
| Contract | Check declared producer and consumer expectations |
| Integration | Exercise databases, brokers and network protocols |
| End to end | Exercise selected user journeys across deployed services |
The number of possible combinations grows quickly.
End-to-end tests alone can become slow, fragile, difficult to diagnose and expensive to maintain.
Strong local tests and contract tests reduce dependence on one enormous test environment. They do not prove complete semantic compatibility, production-data compatibility, latency and capacity behaviour, failure handling, or every end-to-end workflow.
The service boundary creates test seams.
It also creates more things requiring integration evidence.
One monolithic process may start with:
make run
A service architecture might require:
- catalog
- basket
- inventory
- order
- payment
- message broker
- several databases
- gateway
- observability components
A developer working on basket should not always need to operate the entire production topology locally.
Possible approaches include:
- run one service with fakes
- use contract-compatible stubs
- provide a smaller local composition
- connect to selected shared development dependencies
- record and replay representative responses
The production architecture and the developer workflow need to be designed together.
A system that can be deployed independently but cannot be understood independently has only completed half of the work.
Teams and coherent service boundaries
Ownership can justify a service boundary
A service can give one team end-to-end responsibility for a capability.
CATALOG TEAM
│
├── product model
├── catalog API
├── catalog database
├── deployment
├── monitoring
└── incident response
The team can make local decisions without waiting for every other application team.
This can reduce communication overhead where the organisation is large enough to need parallel delivery.
But splitting one small team across many services can have the opposite effect.
Each developer must understand:
- several repositories
- several deployment pipelines
- several local environments
- several operational surfaces
The architecture should reflect actual team and ownership needs.
Creating a service per imagined future team can leave today’s team operating tomorrow’s org chart alone.
Suppose two teams disagree about one shared business capability.
Placing a network API between them does not automatically clarify:
- who owns the rule
- which meaning is authoritative
- how changes are prioritised
- who supports incidents
The service may turn organisational ambiguity into an API dispute.
A healthy boundary needs:
- clear capability ownership
- contract responsibility
- operational responsibility
- change process
- consumer communication
Technology can enforce a decided boundary.
It cannot decide the organisation’s responsibilities by itself.
Size should follow behaviour and invariants
The term microservice encourages size questions.
- How many lines?
- How many endpoints?
- How many tables?
- How many engineers?
There is no useful universal number.
A service should be small enough to represent a coherent capability with understandable ownership.
It should be large enough to preserve the rules and data that belong together.
TOO BROAD
unrelated capabilities
forced release coordination
large ownership surface
TOO NARROW
chatty calls
split invariants
distributed transactions
excess operational overhead
The useful boundary follows:
- business behaviour
- data ownership
- transactional invariants
- change cadence
- team responsibility
A service with five endpoints can be badly bounded.
A service with fifty endpoints can still represent one coherent capability.
Suppose the database has:
- orders
- order_lines
- addresses
- order_status_history
Creating one service per table produces:
- order service
- order-line service
- address service
- status-history service
Now creating one order may require several remote calls.
ORDER SERVICE
│
├──► ORDER-LINE SERVICE
├──► ADDRESS SERVICE
└──► HISTORY SERVICE
A business operation that previously used one transaction has become distributed coordination.
The services are individually small.
The system is not simpler.
These records may belong to one order aggregate and one transaction boundary.
ORDER SERVICE
│
├── orders
├── order lines
├── accepted address
└── status history
Decomposition should follow coherent behaviour rather than the visual neatness of one noun per box.
A distributed monolith pays both sets of costs
A distributed monolith has separate services but requires coordinated behaviour resembling one tightly coupled application.
Warning signs include:
- services must be deployed together
- services share one database schema
- one request crosses many services
- one shared domain package changes everywhere
- local development requires the entire estate
- one service cannot start without all others
- a minor feature touches many repositories
- failures propagate across the whole call chain
The architecture pays for:
- networking
- deployment complexity
- observability
- service discovery
- remote failure
while retaining:
- coordinated releases
- shared internals
- tight change coupling
This is the least attractive combination.
The solution is not always more services.
It may be:
- merge services
- redraw ownership boundaries
- remove synchronous dependencies
- give services private data
- stabilise contracts
Distribution should increase meaningful autonomy.
Extracting services safely
Extract when a proven boundary needs a new lifecycle
One repository and one process can make changes across related functionality straightforward.
- change catalog model
- update basket caller
- migrate database
- run test suite
- deploy one artifact
When the domain is still being discovered, this can be valuable.
Boundaries often move while the team learns:
- which rules belong together
- which data changes together
- which capabilities have distinct lifecycles
- which dependencies are genuinely optional
Extracting services too early can freeze guesses into network contracts.
Changing an internal interface is usually easier than coordinating a remote contract and data migration.
A monolith can support rapid learning, provided it is kept modular enough to reveal potential boundaries.
As an application and organisation grow, one deployment unit can create:
- large test suites
- release contention
- unclear ownership
- high deployment blast radius
- coupled scaling
- slow build times
- frequent merge conflicts
Several teams may need to coordinate changes to one artifact.
A small catalog change may require redeploying payment code.
The application may still be logically modular, but the deployment unit has become a bottleneck.
This is a stronger reason to extract a service:
A proven boundary needs an independent lifecycle that the monolith can no longer provide efficiently.
The service is introduced to solve an observed coupling problem.
Not to make the architecture resemble a fashionable diagram.
Choose a coherent candidate
A good candidate often has:
- clear business ownership
- a stable vocabulary
- private data
- a small supported interface
- distinct change cadence
- distinct scaling or security needs
- limited synchronous dependencies
For example, product-image processing may be a useful early extraction.
APPLICATION
│
│ image job
▼
IMAGE PROCESSOR
It may have:
- CPU-heavy workload
- asynchronous operation
- separate scaling
- few business transactions with the core application
By contrast, separating orders from order lines is likely to cut through one consistency boundary.
The easiest component to draw as a box is not necessarily the safest component to extract.
Migration moves authority as well as code
Suppose catalog begins inside the monolith.
MONOLITH
│
├── catalog
├── basket
└── order
The target is:
MONOLITH
│
├── basket
└── order
CATALOG SERVICE
The transition may require:
- define catalog contract
- separate catalog model
- move ownership of catalog tables
- route selected reads to the service
- migrate writes
- keep old and new paths compatible temporarily
- remove old code and access
During migration, both paths may exist.
Ownership must remain explicit:
One component should remain the authoritative writer for each fact at every stage of the migration.
Shadow reads, comparison and change-data capture may help validate the new path. Uncoordinated dual writes create two failure paths and uncertain authority.
If both the monolith and the new service can change the same facts independently, the migration creates two authorities.
Extraction is a data and responsibility migration, not only a code movement.
During extraction, the monolith may access the service through an adapter.
BASKET MODULE
│
▼
CATALOG ADAPTER
│
▼
CATALOG SERVICE
That bridge should use the service’s supported contract.
A dangerous shortcut is:
- new catalog service
- plus
- monolith still reading catalog tables directly
Now the application depends on both:
- the service API
- the service’s internal storage
The new boundary cannot evolve.
A transitional dependency needs a removal plan.
Otherwise temporary architecture develops tenure.
Rather than replacing the complete monolith at once, capabilities can move gradually.
REQUESTS
│
▼
ROUTING LAYER
│
├──► existing monolith
└──► extracted service
One capability is redirected at a time.
The monolith continues serving the remaining functions.
This can reduce the size of each migration.
It also creates a period in which:
- old and new architectures coexist
- routing must be correct
- data ownership is changing
- observability spans both environments
Incremental migration reduces one kind of risk.
It does not remove the need for careful boundary and data design.
A practical decision framework
Questions before introducing a service
Before introducing a service, I should be able to answer:
| Concern | Question |
|---|---|
| Capability | What coherent business capability will this service own? |
| Data | Which facts and invariants belong to it? |
| Lifecycle | Does it genuinely need independent deployment? |
| Scale | Does it have a distinct traffic or resource profile? |
| Failure | Which failures will the boundary isolate, and which dependencies remain shared? |
| Transactions | Which local transaction will become a workflow? |
| Queries | Which easy joins will become composition or derived data? |
| Contract | Can structure and meaning evolve without coordinated releases? |
| Operations | Who will deploy, monitor and support it? |
| Platform | Are identity, telemetry, delivery and discovery capabilities available? |
| Recovery | How will uncertain and partially completed operations be reconciled? |
| Alternative | Could a module, worker or internal resource boundary solve the current problem without a network boundary? |
If the answers are vague, the service boundary is probably being proposed before its purpose is understood.
A possible furniture-store boundary
Suppose the first version of the furniture store is a modular monolith.
BFSTORE APPLICATION
│
├── catalog module
├── basket module
├── order module
├── inventory module
└── payment integration
Each module owns its internal model and tables.
The application deploys as one process.
This allows the domain to develop without turning every changing assumption into a remote contract.
Later, evidence appears:
- catalog reads dominate traffic
- product search needs independent scaling
- payment requires tighter access controls
- image processing consumes substantial CPU
- notification work should happen asynchronously
- different teams own catalog and fulfilment
These are concrete pressures.
The system can then choose different responses.
Extract image processing
CATALOG
│
│ image job
▼
MESSAGE SYSTEM
│
▼
IMAGE PROCESSOR
Why:
- distinct workload
- asynchronous operation
- clear input and output
- independent scaling
Extract payment
ORDER
│
│ payment operation
▼
PAYMENT SERVICE
Why:
- clear security boundary
- external provider integration
- specialised lifecycle and auditing
Costs:
- ambiguous network outcomes
- idempotency
- reconciliation
- distributed checkout workflow
Keep order and order lines together
ORDER MODULE OR SERVICE
│
├── order
├── order lines
├── accepted delivery address
└── status history
Why:
- one coherent model
- shared invariants
- one transaction boundary
The result is neither “everything monolithic” nor “everything a microservice”.
It is a system whose boundaries are paid for where their benefits are visible.
The mental model I am keeping
My old model was:
MONOLITH
simple but bad
MICROSERVICES
complex but modern
The new model is:
MODULE BOUNDARY
│
├── explicit ownership
├── private model
├── supported interface
└── shared deployment and runtime lifecycle
NETWORK SERVICE BOUNDARY
│
├── explicit ownership
├── private model and data
├── versioned remote contract
├── independent deployment
└── independent runtime lifecycle
The service boundary can buy:
- deployment autonomy
- scaling autonomy
- failure containment, subject to shared dependencies
- security isolation
- team ownership
- selective technology freedom where it justifies the support cost
The invoice contains:
- latency
- partial failure
- timeouts
- retries
- ambiguous outcomes
- distributed transactions
- eventual consistency
- contract evolution
- service discovery
- workload identity
- distributed observability
- operational overhead
So the decision is not:
Is a monolith or a service architecture better?
The useful question is:
Which capability needs an independent lifecycle strongly enough to justify a network boundary?
Sometimes the answer will be:
catalog service
Sometimes:
payment service
Sometimes:
not yet
And sometimes:
never, because a well-protected module is the correct boundary
A network boundary is one of the strongest architectural boundaries available.
It can enforce autonomy that package conventions alone may struggle to preserve across large organisations.
It can also transform one understandable application into a collection of individually healthy processes that cannot complete a customer order together.
The boundary should therefore arrive with a job.
It should isolate a capability, enable a lifecycle or solve a measured constraint.
Without that job, the network is not architecture.
It is distance.
References and further reading
Microservice architecture
Martin Fowler and James Lewis: Microservices Describes the microservice architectural style, including independently deployable services, organisation around business capabilities, decentralised data management and infrastructure automation.
Martin Fowler: Microservice Trade-Offs Examines the benefits and costs of service boundaries, including strong modularity, independent deployment, distribution, eventual consistency and operational complexity.
Martin Fowler: Microservice Prerequisites Highlights operational capabilities that should exist before adopting a large microservice estate, including rapid provisioning, monitoring and deployment automation.
Starting and evolving system boundaries
Martin Fowler: Monolith First Presents the argument for beginning with a monolith while a domain is still being understood, then extracting services as stable boundaries and independent-lifecycle needs emerge.
Zhamak Dehghani: How to break a Monolith into Microservices Describes incremental decomposition of a monolith by business capability and the importance of decoupling code and data deliberately.
Martin Fowler: Strangler Fig Application Describes replacing an existing system gradually by routing selected functionality to new implementations rather than performing one complete replacement.
Service-owned data
Microsoft: Data sovereignty per microservice Explains why an autonomous service owns its domain logic and data, and describes the loss of local joins and single ACID transactions when data is distributed across services.
HTTP service contracts
RFC 9110: HTTP Semantics Defines HTTP methods, status semantics, content, safe and idempotent requests, and the meaning of application responses.
RFC 9112: HTTP/1.1 Defines HTTP/1.1 messaging, connection management and incomplete-message behaviour.
These specifications supersede RFC 7230 and RFC 7231, which were current when the original article was written.
RPC lifecycle
gRPC: Core concepts, architecture and lifecycle Introduces gRPC service definitions, unary and streaming calls, metadata, deadlines, cancellation and the client and server views of an RPC lifecycle.
gRPC and Deadlines Explains why remote calls require deadlines and why a client can stop waiting before a server reaches its own conclusion about the call.