Applied architecture stage summary
The Applied stage is where architecture stops being a drawing and starts costing money. It takes the six decisions that most often separate a system that can be changed from one that cannot: whether to distribute at all, who owns which data, whether services talk by asking or by announcing, how the write side and the read side are allowed to diverge, where the boundaries of a model actually fall, and what the system does at the moment a dependency stops answering.
One argument runs through all six modules. Every pattern in this stage buys something specific and charges for it in a different currency, and the charge is always operational. Microservices buy independent deployment and charge in pipelines, on-call rotations and distributed data. Events buy producer independence and charge in duplicate delivery and schema discipline. CQRS buys read performance and charges in eventual consistency the interface has to show. Circuit breakers buy containment and charge in tuning that nobody does until an incident forces it. A pattern applied without naming what it is buying is a cost with no matching benefit, and that is the single most common failure the stage teaches you to spot.
The sections follow the stage's teaching order, two per module, so you can read straight through to rebuild the stage in your head or jump to the concept you need. Each section links back to its module for the full treatment.
What you carry out of this stage
- State the three properties a service must have to count as a microservice, and use them to tell a microservice architecture apart from a distributed monolith
- Decide between a monolith, a modular monolith and microservices from team structure, deployment cadence and domain clarity rather than from fashion
- Design a distributed write that spans three services using a saga with compensating transactions, and say what the system looks like during the inconsistent window
- Choose between a log-based and a queue-based broker for a given workload, and explain why the choice follows from whether many independent consumers need the same events
- Make a consumer idempotent, choose a partition key that preserves the ordering you actually need, and classify a schema change as backward compatible, forward compatible or breaking
- Separate CQRS from event sourcing, adopt either on its own, and name the specific business requirement that justifies the complexity of each
- Draw bounded contexts from the language the business uses, size an aggregate around the invariants that must hold in one transaction, and pick the integration pattern for each relationship on a context map
- Contain a failing dependency with a circuit breaker, a bulkhead, an explicit timeout and a named fallback, and say which of those four each incident needed
- Identify the actual bottleneck before choosing a scaling pattern, and order caching, read replicas, queues and sharding by cost rather than by reputation
A microservice is three properties at once, and the decision to have any is organisational
A microservice implements a single business capability, runs in its own process, and owns its own data store, and all three are required at the same time. Two services sharing a database are not independently deployable, because a schema change in one breaks the other, and a service that handles both authentication and order fulfilment has already given up the single-capability property. Martin Fowler and James Lewis named the style in a 2014 article, and the property that separates it from earlier service-oriented approaches is exactly this insistence on independent deployability and data ownership rather than a shared bus.
The spectrum runs monolith, modular monolith, microservices. A monolith deploys as one unit from one codebase over one database, and Shopify runs a commerce platform of enormous scale on a Rails monolith by holding modular discipline inside the single codebase. A modular monolith is one process with real internal boundaries where modules talk through explicit interfaces rather than reaching across domain lines, so it keeps the single deployment while removing most of the coupling that makes monoliths painful to change. Microservices sit at the far end and pay the full price of distribution: network failures, distributed transactions, and observability that has to be built rather than assumed.
The choice is therefore about the organisation, not the code. Sam Newman's threshold, that you must be this tall to use microservices, means a working CI/CD pipeline, tracing across services and a team that can operate distributed systems, all in place before decomposition begins. Netflix took four years and built Hystrix, Eureka, Zuul and Chaos Monkey along the way. The practical consequence is that when a team cannot name the specific pain point it is buying out of, independent release cadence across several teams, dramatically different scaling per component, or genuinely mandatory technology diversity, the honest answer is a modular monolith and an extraction plan for later.
Monolith, modular monolith, microservices: one trade-off space
The three styles are read down the same three measures: team autonomy, operational cost and change cost. Microservices buy high autonomy with expensive operations, which leaves the modular monolith, medium autonomy at cheap operations, as the middle option most teams skip past.
Pick the cell whose operational and change costs you can absorb today; do not promote yourself up a ladder. Source: Fowler 2015 Monolith First; Newman 2021 Building Microservices 2nd ed.; Richards and Ford 2020.
Once each service owns its data, a transaction becomes a sequence you have to be able to undo
Database per service is what makes independent deployment real, and it is also the hardest part to operate. An order placement that was three rows in one ACID transaction becomes three writes in three separate stores: the Order service creates the order, the Inventory service reserves the items, the Payment service takes the money. If payment fails after inventory has been reserved, nothing rolls it back for you, and the system is sitting in a state no single database ever allowed.
The saga pattern is the answer taught here. A saga is a chain of local transactions, each one publishing an event that triggers the next, and each one carrying a compensating transaction that undoes it. Inventory publishes ItemsReserved; Payment fails and publishes PaymentFailed; Inventory listens for that and runs ReleaseReservation. The system is genuinely inconsistent for the duration of the chain, and that is the deliberate trade. Two-phase commit would preserve immediate consistency but reintroduces distributed locks and coordination coupling, which is precisely the independence the decomposition was bought for.
The same ownership rule decides how one service reads another service's data. A second service that needs customer information calls the owning service's API or subscribes to its change events and keeps its own local read model. It does not query the other service's database and it does not set up replication of the other service's tables, because both create an implicit shared schema that no deployment pipeline can protect. Around all of this sits the operational layer the module treats as required rather than optional: service discovery so instances can find each other as they scale and restart, and a service mesh carrying mutual TLS, routing, retry policy and tracing as a sidecar so that services in different languages get identical policy without each one implementing it.
A service boundary is operable only when four facets cross together
API, Data, Runtime and Telemetry each carry what crosses the boundary and the evidence the receiving team needs, so a service handed over with an OpenAPI spec but no alert routes wired reaches the on-call rota with nothing to page on.
Drop any facet and the boundary becomes a service the team cannot operate. Source: Newman 2021 Building Microservices ch.12; Beyer et al. 2018; OpenTelemetry.
An event is a fact about the past, and that grammar is what removes the coupling
In a request-driven call, service A knows service B exists, knows its API, and inherits its availability: if B is slow A waits, if B is down A fails. In an event-driven design A publishes a fact, that an order was placed, and does not know who reads it. B subscribes and reacts. If B is slow, A is unaffected; if B is down, the events wait. The test of whether a system is genuinely event-driven is simple and unforgiving: when a new consumer is added six months after launch, how many changes does the producer make? The correct answer is none.
Three message types have to be kept apart. Domain events are past-tense facts inside one bounded context, such as OrderPlaced or StockLevelChanged, owned by the service that produced them. Integration events translate a domain fact for consumption outside that context, so an OrderPlaced domain event becomes something like OrderConfirmedForShipping for logistics, which stops the logistics vocabulary bleeding back into the order model. Commands are imperative and addressed to a named receiver, such as ProcessPayment, and they fail if that receiver refuses or is absent.
The distinction is not naming hygiene. A system built from commands wearing past-tense names still carries command coupling: the producer has a list of who must act, and adding a consumer means changing the producer. Past-tense naming is the cheapest available signal that a message is a fact rather than an instruction, so if a message name reads as a verb in the imperative, treat the coupling as request-driven no matter which broker it travels on.
Sync and async are different coupling models, not different speeds
Six dimensions run down the table, from coupling in time to debugging, with coupling in availability emphasised: a synchronous call hands a peer's downtime to its caller, while an asynchronous one buffers it through the broker and pays in ordering it must make explicit.
Choose sync when the caller needs to know; choose async when the caller does not need to wait. Source: Hohpe and Woolf 2003 EIP; Newman 2021 ch.5; Fowler 2017.
The broker model, the partition key and the schema rule are one decision each, and all three bite in production
Kafka is log-based. Events are appended to a partitioned, replicated commit log, each consumer group tracks its own offset, retention is configured in time rather than in deliveries, and several independent consumer groups can read the same events at their own pace. That is what makes replay possible: a service deployed today can read history from the beginning. RabbitMQ is queue-based. Messages go to queues, are deleted on acknowledgement, and the broker holds the delivery state, which makes it the natural fit for work distribution where one item must be handled by exactly one worker. AWS EventBridge is a managed serverless bus for routing events to targets inside a cloud-native estate. Support tickets that must be handled once go to a queue; an activity stream that four teams each need in full goes to a log.
Two Kafka mechanics decide whether the design survives contact with retries. Ordering is guaranteed within a partition and nowhere else, and the partition is chosen by hashing the message key, so using the order id as the key is what keeps every event for that order in sequence. Delivery is at-least-once by default, so consumers must be idempotent: a consumer that inserts a payment row per delivery will eventually create a duplicate payment, while one that upserts on the event id will not. Exactly-once semantics exist, requiring producer idempotence and transactional consumers, and most systems decline the latency and operational cost and rely on idempotent consumers instead.
Schemas then have to change without breaking anyone. Backward compatible means the new schema can read old data, which is what adding a field with a default gives you. Forward compatible means an old schema can read new data, which holds when consumers ignore fields they do not recognise. Renaming a field, changing its type or removing a required field is breaking, and the registry rejects it rather than letting it reach a topic that holds months of history. The working habit is to treat a topic as a published interface with a longer life than any consumer on it.
An event is a contract held across every reader
One producer publishes an event carrying schema, semantics, ordering, delivery and retention, and three readers take the same event, so changing any one surface breaks the application, the warehouse and the audit trail together.
Break any of the five surfaces and you break every reader at once. Source: Hohpe and Woolf 2003 EIP; Fowler 2017 EDA; CloudEvents 1.0.2.
CQRS is one model split in two, and the bill arrives as eventual consistency in the interface
Bertrand Meyer stated the principle in 1988: a method should either change state and return nothing, or return data and change nothing, never both. Greg Young raised it to an architectural rule in 2010 as Command Query Responsibility Segregation. The command side handles state changes such as PlaceOrder or SubmitVATReturn and enforces the business rules; the query side answers reads and is shaped for the questions actually being asked. In its mildest form CQRS is two code paths in one service over one database, and even that improves clarity. The stronger form adds a separate read store.
A read model, also called a projection, is a denormalised structure precomputed for one query pattern and updated from the events the write side emits. A dashboard that would otherwise join several tables and compute aggregates on every request becomes a single stored document per user, and latency drops from hundreds of milliseconds to single digits. That is the whole benefit, and it is real when the read and write loads are genuinely asymmetric.
The cost is eventual consistency, and it lands in the user interface rather than in the database. Between the event being published and the projection being updated there is a window in which the read model is stale, so the user who just submitted something and refreshed immediately sees the old figure. That is designed behaviour, not a defect, and the design owes them a response to it: show a processing state, poll until the projection catches up, or route that user's own immediate reads to the write side. If nobody decides which, the support queue decides for you.
The read model is shaped by queries; the write model by invariants
Aggregate, invariant and event on the left pair with query shape, projection and cached view on the right, and every arrow runs left to right, so a new screen can reshape the read side without touching an invariant.
The projection is the bridge; without it the two columns drift and the system loses CQRS's payoff. Source: Young 2010 CQRS Documents; Fowler 2011 CQRS; Vernon 2013 ch.6.
Event sourcing makes history the storage format, which is why it is justified by regulation more often than by scale
Conventional persistence stores current state and overwrites the previous value, so history exists only if somebody built an audit log beside it. Event sourcing stores the sequence of events that produced the state as an immutable, append-only log, and the current state is derived by replaying that stream: OrderCreated, ItemAdded three times, PaymentProcessed, OrderShipped. The log is the source of truth and the state is a projection of it, which is why the phrase events instead of state misstates the model. State is always there; it is just derived on demand.
Three capabilities follow that state-based storage cannot give you. Audit history is intrinsic rather than bolted on. Temporal queries become possible, because the state at a past moment is the replay up to that point. And projections can be rebuilt from scratch when a read model is corrupted or a new query pattern appears. An event store organises events into one stream per aggregate and appends with an expected version, so two writers racing to append the same position get one success and one concurrency conflict. Snapshots exist purely as a performance measure once streams grow long, and the log remains authoritative behind them. EventStoreDB is purpose-built for this, and a PostgreSQL table keyed by stream and position with optimistic locking is a legitimate alternative when the team already runs PostgreSQL.
The reason this matters more as a filter than as a technique is that both patterns are over-applied. Greg Young's own guidance is that CQRS suits a small minority of bounded contexts, and event sourcing a smaller minority still. Each adds real work: schema management for events, a snapshot strategy, projection rebuilds, and a learning curve. The question to ask before adopting either is whether full audit history, temporal queries or projection rebuilding is a stated business or regulatory requirement. For a content management system with no audit obligation, the answer is no and a conventional database is the correct design.
The boundary is linguistic before it is technical, and the aggregate is where consistency is enforced
Eric Evans published Domain-Driven Design in 2003 after watching the same failure repeatedly: teams sharing a codebase used the same word for different things. Sales meant one thing by customer, finance another, support a third, and the single shared class grew fields for all three until every change for one team broke another. The diagnosis was that the problem was linguistic and the code had no boundary marking where each meaning applied. A bounded context is that boundary: inside it, a term has exactly one meaning, and the ubiquitous language it defines is used in speech, tests and code without translation. Where a method named processPayment could mean three things, settleTrade with a settlement date can be read and confirmed by a domain expert.
Contexts should follow team ownership, which is Conway's Law used deliberately rather than suffered. If the billing team owns the billing context, it owns the customer model inside it and no other team changes that model without agreement. A bounded context is not the same thing as a microservice: the context is a conceptual boundary drawn from the domain, the service is a deployment unit, and one context may be several services or several small contexts may share one. Identify the contexts first, then decide the deployment shape, because starting from services and fitting contexts to them reliably produces the wrong boundaries.
Inside a context, the aggregate is the consistency boundary. It is a cluster of objects with one root through which all external access passes, so the invariants, such as no line item with quantity zero and no items added to a confirmed order, are enforced in one place instead of being scattered through application services. Changes inside an aggregate are atomic; changes across aggregates are eventually consistent through domain events. Small aggregates are strongly preferable, because a large one becomes a contention hotspot: an Order aggregate that also holds the customer profile, the payment method and the shipping history will be locked by checkout, payment, fulfilment and profile edits at the same time. Size the aggregate by asking what must be true together in one transaction, not by which tables are related.
Bounded contexts: the same word, four different meanings
Customer means a prospect in Sales and an account holder in Billing, and each row is owned by a different team, so a single shared definition would force four owners to agree on attributes the others do not carry.
Each context owns its language; do not invent a 'universal Customer' that breaks every team's model. Source: Evans 2003 Domain-Driven Design; Vernon 2013 Implementing DDD ch.2.
A context map makes the integration pattern a decision instead of an accident
A context map shows every bounded context in the system and the relationship between each pair, and its value is that it forces those relationships to be chosen before code makes them permanent. A shared kernel means two contexts co-own a small part of the model and neither team changes it alone. A customer-supplier relationship means an upstream context publishes an interface that a downstream context consumes, with changes negotiated between the two teams. An anti-corruption layer means the downstream context translates the upstream model into its own at the boundary.
For service-based systems the anti-corruption layer is the one to reach for most often. When the ordering context publishes OrderConfirmed, the fulfilment context should not adopt the ordering context's data model; it should translate the event into its own FulfilmentRequest. The payoff is contained change: when ordering alters its event schema, only the translation layer moves and the fulfilment model is untouched. Without that layer, the upstream model quietly becomes the downstream model, and the boundary that the context map drew exists on the diagram and nowhere else.
Seven DDD context-map relationship types
The seven are Partnership, Shared Kernel, Customer-Supplier, Conformist, Anticorruption Layer, Open Host Service and Separate Ways. Conformist and Anticorruption Layer answer the same upstream in opposite ways: take the model as it stands, or pay for translation at the border.
Pick the relationship by the shape of the dependency, not by what is easy to integrate this sprint. Source: Evans 2003 Domain-Driven Design pt.IV; Vernon 2013 ch.3.
Cascading failure is a resource problem, so the fix is containment rather than prevention
Peter Deutsch and colleagues at Sun Microsystems wrote down the eight fallacies of distributed computing from 1994: the network is reliable, latency is zero, bandwidth is infinite, the network is secure, topology does not change, there is one administrator, transport cost is zero, the network is homogeneous. All eight are false, and the module singles out three as the most consequential for design: that the network is reliable, when packets are lost and connections drop; that latency is zero, when any call can become arbitrarily slow under load; and that topology does not change, when instances restart and move address as they scale in and out. A system built on them behaves perfectly in staging, where the network is local, and fails unpredictably in production.
The failure mode to understand is mechanical. When a downstream service becomes slow rather than dead, every call to it holds a thread waiting for a timeout that may be thirty seconds away. Fifty threads, fifty blocked calls, and the calling service now cannot serve requests that have nothing to do with the slow dependency. A circuit breaker, named by Michael Nygard in Release It! and made famous by Netflix Hystrix, watches the failure rate and opens when it crosses a threshold. Closed means calls pass and failures are counted; open means calls fail immediately with a fallback and no network call is attempted; half-open lets a single probe through after a timeout and closes on success or reopens on failure. What it really does is convert a slow failure into a fast one, which is what gives the threads back.
A bulkhead solves the other half. Named after the compartments of a ship's hull, it gives each downstream dependency its own resource pool, so a dependency that fills its pool with blocked calls cannot take the pools used for everything else. Separate connection pools, separate thread pools, or separate worker pools in an event loop all express the same idea, and each pool should be sized to the downstream service's capacity under normal load rather than to the caller's maximum concurrency. The two patterns answer different questions and a serious incident usually needs both: the breaker stops wasting time on a failing dependency, and the bulkhead stops that dependency's exhaustion spreading.
Four nested failure-containment scopes: Process, Service, Cluster, Region
Each scope carries the recovery that lives at it, a restart at the process and a failover at the region, so answering a crashed process at regional scope moves far more users than the fault ever touched.
Pick the smallest scope that contains the fault; over-scoping creates a wider outage than necessary. Source: Nygard 2018 Release It! 2nd ed.; Beyer et al. 2018 SRE Workbook ch.22.
Every call gets a timeout, every retry needs an idempotency story, and every dependency needs a named fallback
A network call with no timeout blocks forever when the far end never answers, and in a service chain that block propagates upwards: checkout waits on inventory, which waits on pricing, which has no timeout on its database query. Timeouts belong at each layer, and the outer one has to be longer than the inner one so that a retry has room to happen. The module's working ranges are a user-facing API in the low seconds, an internal service call from several hundred milliseconds to a couple of seconds, and a database query in the low hundreds of milliseconds.
Retries are for transient failures only, the network glitch that clears in milliseconds, and they need exponential backoff with jitter so that every client does not return at the same instant and produce a thundering herd against a service that was just recovering. The harder constraint is correctness: a non-idempotent operation must not be retried without an idempotency key. If the order was created and only the response was lost, a retry without a key creates a second order and a second charge, and no amount of backoff prevents that. The server records the key, recognises the duplicate and returns the original response.
Graceful degradation is the part that gets skipped. It means deciding, in advance, what a reduced answer looks like for every external dependency: popular items from cache when the recommendation service is unavailable, rather than an error page. Combined with chaos testing, where instances are terminated on purpose to prove the system keeps serving, the stance the stage argues for is that resilience is demonstrated continuously rather than assumed. An untested circuit breaker with library default thresholds is decoration, and the time to find that out is not during the incident.
Resilience controls compose: each catches what the previous let through
The five controls are ordered so that each row catches what the row above let through, and the callout names the dependency, so a bulkhead added without a circuit breaker still lets retries hammer a peer that is already failing.
Missing one row in the stack collapses everything below it; the order is the safety net. Source: Nygard 2018 Release It! 2nd ed.; Beyer et al. 2016 Site Reliability Engineering.
Scaling patterns are cheap or expensive in a fixed order, and the ordering is the skill
Vertical scaling adds capacity to one machine and needs no application change, which makes it the correct first move more often than its reputation suggests; its limits are the hardware ceiling and the single point of failure. Horizontal scaling adds instances behind a load balancer, has no theoretical ceiling and survives the loss of any one instance, but it has a hard prerequisite: the application must be stateless. If a session lives in one server's memory, the load balancer cannot route the next request anywhere else, and the user appears logged out. All state has to be externalised, sessions to a store such as Redis, files to object storage, data to the database, so that any instance can serve any request.
Caching is the highest-impact single change for read-heavy systems and the most common source of quiet bugs, because a stale cache returns wrong data with no error at all. Cache-aside is the usual shape: check the cache, on a miss read through to the database, store with a time to live, return. It applies at several layers, a CDN at the edge for static assets and pages, a gateway for public idempotent GETs, and an application cache for query results. The invalidation strategy has to be designed with the cache and not after the first stale-content complaint: time to live expiry is simplest, write-through keeps the cache current at the cost of write overhead, and event-driven invalidation deletes the specific key the moment the underlying fact changes.
For the database, read replicas and sharding solve different bottlenecks at very different prices. Replicas take read load off the primary and cost you replication lag, which is fine for a catalogue and not fine for a user reading back their own just-completed write, which must go to the primary. Sharding partitions the data across primaries and is the only answer to a write-throughput or data-volume ceiling, but cross-shard queries become application code, a badly chosen partition key means migrating data to rebalance, and transactions across shards need distributed coordination. Instagram is the worked example: tens of millions of users on a three-person backend team using stateless application servers behind a load balancer, PostgreSQL read replicas, Redis and a CDN, with nothing invented and every pattern aimed at the bottleneck that was actually binding at the time.
Scaling moves the bottleneck through four named stages
Each card names the cause and then the limit its fix exposes, CPU into memory, memory into I/O and I/O into coordination, so the sequence does not end at the fourth card: it ends at the team.
Scaling moves the bottleneck rather than removing it; plan for the next stage before you finish the current one. Source: Patterson and Hennessy 2017 ch.1; Beyer et al. 2018 SRE Workbook ch.6; Amdahl 1967.
A queue separates the rate you accept work from the rate you can do it, and queue depth is the signal to run on
Queue-based load levelling treats acceptance and processing as two independent design concerns. During a spike the endpoint accepts at the incoming rate and returns immediately, while workers consume at whatever rate they can sustain, and the queue holds the difference. Without one, a spike that doubles arrivals forces you either to double capacity instantly, which is rarely possible, or to drop requests. With one, the spike costs latency for the work submitted during the peak instead of failures.
Queue depth is the operational signal that matters, because a growing queue is the direct statement that workers are behind. Auto-scaling workers on queue depth is the standard cloud arrangement, and the threshold should come from the latency objective for that work rather than from a round number. The pattern fits asynchronous work where the caller does not need the answer in the same breath: image processing, report generation, email, exports. It does not fit work the user is waiting on in the current session, which needs a synchronous response or an explicit polling design.
The stage closes on the same discipline in a different form. Design for partition tolerance rather than assuming a network that never fails, because the test environment never partitions and production eventually does, and a system built for the happy path produces inconsistent results that nobody can explain afterwards. Across every pattern in the stage the rule holds: identify the bottleneck or the failure mode by measurement, then apply the pattern that answers it. A pattern applied to an unnamed problem adds complexity and solves nothing.
Capacity is a four-stage loop: measure, forecast, plan, act
Measure, forecast, plan and act run in order and the dashed arrow returns at the next measurement window, making every act on capacity part of the signal the next forecast is built from.
Capacity is a feedback loop; close it at every window or the system drifts away from the SLO. Source: Beyer et al. 2018 SRE Workbook ch.5; Forsgren et al. 2018 Accelerate.
The traps this stage warns against
Adopting microservices for future flexibility, before there is a CI/CD pipeline, tracing or a settled domain model.
Instead: Name the pain point being bought out of: independent release cadence across several teams, dramatically different scaling per component, or mandatory technology diversity. If none applies, build a modular monolith with explicit internal interfaces. Boundaries that are wrong in a monolith are a refactor; boundaries that are wrong across services mean rewriting data ownership, event contracts and pipelines for every service affected.
Publishing commands with past-tense names and calling the result event-driven.
Instead: Apply the producer test: when a new consumer appears, does the producer change? If it does, the coupling is still request-driven whatever the broker. Domain events are facts owned by the producing context, integration events are the translated form for other contexts, and commands are instructions to a named receiver.
Assuming the broker guarantees exactly-once delivery, so consumers can act on every message as though it were unique.
Instead: Kafka's default is at-least-once. Exactly-once semantics need producer idempotence plus transactional consumers and buy latency and operational cost. Make the consumer idempotent instead: upsert on the event id so a redelivery produces the same result. And remember ordering holds within a partition only, so the partition key is what preserves the sequence you care about.
Treating CQRS and event sourcing as a single package, so adopting one drags in the other.
Instead: They are independent. CQRS separates command handlers from query handlers and can run on one conventional database. Event sourcing changes the persistence model to an append-only log and can be used without CQRS. They combine well because an event-sourced write side already emits what projections need, but neither requires the other, and adopting both at once doubles the complexity you have to justify.
Drawing bounded contexts to match the services that already exist, or assuming one service equals one context.
Instead: A bounded context is a conceptual boundary set by language and ownership; a service is a deployment unit. Find the contexts from the words the business uses and from which team owns each concept, then choose the deployment shape. One context may be several services, and several small contexts may share one.
Adding retries to a failing call and treating that as fault tolerance.
Instead: Retries answer transient failures; circuit breakers answer persistent ones, and retrying a service that has been degraded for a minute adds load to something already struggling. Use both for what they are, add exponential backoff with jitter, and never retry a non-idempotent operation without an idempotency key, or a lost response becomes a second charge.
Reaching for sharding, or for a database change, as the first response to a load problem.
Instead: Measure the bottleneck first. Most read pressure is answered by a CDN, a cache with a designed invalidation strategy, and read replicas, at a fraction of the cost. Sharding answers a write-throughput or data-volume ceiling specifically, and charges for it in application-level cross-shard queries, rebalancing migrations and distributed transactions.
Treating a stale read model, or the inconsistent window inside a saga, as a defect to be engineered away.
Instead: Both are designed properties of the trade that bought independence and read performance. Handle them in the interface and the process: show a pending state, route a user's own immediate reads to the write side, and give every saga step a compensating transaction so the system can be returned to a consistent state rather than left mid-chain.
Core distinctions
- Monolith versus modular monolith versus microservices: one deployment over one database, one deployment with enforced internal interfaces, and many independently deployable services each owning its own data; only the third pays for distribution, and only the third buys independent release
- Microservice versus distributed monolith: the label is earned by single business capability, own process and own data store held at once, and a set of services sharing a database is a monolith that has added network calls to its failure modes
- Event versus command versus integration event: an event is a past-tense fact whose producer does not know its consumers, a command is an instruction to a named receiver that fails if the receiver refuses, and an integration event is a domain fact translated for a different bounded context
- Log-based versus queue-based brokers: Kafka retains a partitioned log that many consumer groups read independently and can replay, while RabbitMQ deletes a message once acknowledged and routes each one to a single worker, which is why one suits shared streams and the other suits work distribution
- At-least-once versus exactly-once: at-least-once is the default and makes idempotent consumers mandatory, while exactly-once requires producer idempotence and transactional consumers and buys its guarantee with latency and operational complexity
- Backward versus forward compatibility: backward means a new schema can read old data, which adding a field with a default gives you; forward means an old schema can read new data, which holds when consumers ignore unknown fields; renaming, retyping or removing a required field is neither
- CQRS versus event sourcing: CQRS separates the write model from the read model, event sourcing makes an append-only event log the source of truth, and either can be adopted without the other
- Aggregate versus entity or table: an aggregate is drawn around the invariants that must hold in one transaction and may span several tables, while normalisation is drawn around storage; a large aggregate is a contention hotspot rather than a richer model
- Bounded context versus microservice: the first is a boundary of meaning and ownership drawn from the domain, the second is a unit of deployment, and the mapping between them is a decision rather than an identity
- Circuit breaker versus bulkhead versus retry: the breaker turns a slow failure into a fast one, the bulkhead stops one dependency's exhaustion reaching the other pools, and the retry only addresses a failure that will clear on its own within milliseconds
- Read replica versus shard: a replica adds read capacity and costs replication lag, so a user's own just-written data must still be read from the primary, while a shard adds write capacity and costs cross-shard queries, rebalancing and distributed transactions
- Vertical versus horizontal scaling: vertical needs no application change and ends at a hardware ceiling with one point of failure, while horizontal has no ceiling but requires statelessness, since local session state stops the load balancer routing the next request anywhere else
That is the Applied stage in one place. A decision about distribution made from team structure rather than fashion, data ownership that turns transactions into sagas with compensating steps, events that remove producer coupling and the broker, ordering and schema rules that make them survivable, CQRS and event sourcing as two separate patterns each with its own justification, bounded contexts and aggregates that put the boundary where the language and the invariants are, containment patterns that assume failure instead of preventing it, and scaling patterns applied in cost order to a measured bottleneck. The applied scenario practice now puts those trades under pressure with situations that have no clean answer, which is where the difference between knowing a pattern and being able to price it shows up.
Sources and further reading
- Fowler, M. and Lewis, J. MicroservicesThe article that named the style and set the properties the stage uses to test it: independent deployability, data ownership and communication by lightweight mechanisms.
- Richardson, C. Pattern: SagaThe pattern catalogue entry behind the distributed transaction treatment: local transactions chained by events, with compensating transactions undoing completed steps when a later one fails.
- Apache Kafka documentationPrimary reference for the log model used in this stage: partitions and keys, consumer groups and offsets, retention, ordering within a partition, and the delivery guarantees including producer idempotence.
- Confluent Schema Registry, Avro compatibilityThe official definitions of backward, forward and full compatibility, and the rules that make adding a defaulted field safe and renaming a required field a breaking change.
- Fowler, M. CQRSFowler's statement of the pattern: one conceptual model split into separate models for updating and for reading. He notes that keeping those separate models consistent raises the likelihood of eventual consistency, and urges caution, since the complexity is earned only in a minority of cases, complex domains and applications that need reads and writes to scale independently.
- Fowler, M. Event SourcingThe source for the three capabilities event sourcing provides that state-based persistence cannot: queryable history, reconstructable past states, and replay into new projections.
- Evans, E. Domain-Driven Design, Addison-Wesley, 2003The origin of the ubiquitous language, the bounded context, the aggregate and the context map. Read Part I before drawing a single boundary.
- Fowler, M. DDD AggregateThe concise statement of the two aggregate constraints the stage relies on: load and save whole aggregates, and do not let a transaction cross an aggregate boundary.
- Fowler, M. BoundedContextThe short explanation of why the same term carries different models in different parts of a system, and how context boundaries relate to team ownership.
- Nygard, M. Release It! Design and Deploy Production-Ready Software, 2nd editionThe stability patterns section is where the circuit breaker and the bulkhead were written down from real incidents. Read it for the failure mechanics, not just the pattern names.
- Principles of Chaos EngineeringThe formal statement behind the practice of terminating instances deliberately to demonstrate that a system keeps serving, rather than assuming it would.
- Queue-Based Load Leveling pattern, Microsoft Azure architecture patternsThe pattern reference behind the final section: a queue placed between a task and the service it invokes, so that intake is decoupled from processing rate. It states that when the producer rate exceeds the consumer rate the queue keeps growing and latency rises, so queue depth should be monitored and consumers scaled within safe limits, and that the pattern does not suit a caller that needs a low-latency synchronous response.