In the previous lesson we put PideYa's monolith in order from the inside: clean layers, ports, adapters, repositories. But success brings a new problem: the delivery team cannot deploy without coordinating with the payments team, a traffic spike in the catalog forces scaling the whole application, and every release is an event. The industry's answer is to split the system into microservices: small, independently deployable processes that communicate over the network. That decision is not free — every method call becomes a remote call that can fail — and that is why a whole catalog of patterns exists for it. As you will see, many are old GoF acquaintances stretched across the network.
Contents
- How to split the monolith: bounded contexts
- API Gateway and Backend for Frontend
- Service Registry and Discovery
- Circuit Breaker
- Retry with backoff and idempotency
- Saga: transactions across services
- Strangler Fig: migrating without a big bang
- Database per Service and its consequences
How to split the monolith: bounded contexts
The first possible mistake is splitting badly. The cuts are not made along technical layers ("database service", "logic service") but along business capabilities. Domain-Driven Design (DDD, Eric Evans) — which we only mention here — calls each boundary within which a model has a coherent meaning a bounded context. In PideYa, "dish" means different things to the catalog (photo, description, allergens) and to the kitchen (preparation time, station): they are different contexts.
PideYa's split ends up like this:
| Service | Responsibility | GoF patterns it takes from the monolith |
|---|---|---|
| Catalog | Menu, prices, availability | Composite (menu), Flyweight, Visitor |
| Orders | Order life cycle | State, Builder, validation Chain |
| Payments | Charges and refunds | Adapter/Abstract Factory for gateways |
| Delivery | Assignment and tracking | Mediator (DispatchCenter), Strategy |
| Notifications | Email, SMS, push | Bridge, Decorator (retries, logging) |
Notice: each service takes with it the internal patterns we built in modules 2-4. Microservices change the between, not the inside.
API Gateway and Backend for Frontend
With five services, should the mobile app know five URLs, five authentication schemes, five error formats? No: you put an API Gateway in front — a single entry point that routes, authenticates, throttles traffic, and aggregates responses.
Recognize the intent? It is the Facade from 03-06 at network scale: hiding a complex subsystem behind a simple interface. CheckoutFacade simplified calls between objects; the gateway simplifies calls between processes.
flowchart LR
APP[Customer app] --> GW[API Gateway]
WEB[Web] --> GW
RID[Courier app] --> BFF[Courier BFF]
GW --> CAT[Catalog]
GW --> PED[Orders]
GW --> PAG[Payments]
BFF --> PED
BFF --> REP[Delivery]
When each client type needs very different aggregations (the courier app wants routes and assigned orders; the customer web wants the menu and tracking), a single gateway bloats until it becomes a team bottleneck. The Backend for Frontend (BFF) pattern creates one gateway per client type, maintained by that frontend's team.
Service Registry and Discovery
In the monolith, calling the payments module was gateway.charge(...). Now Payments is three instances whose IPs change with every deployment. The Service Registry (Eureka, Consul, or Kubernetes DNS) is a directory where each instance registers itself on startup and from which clients obtain fresh addresses (discovery). It is the old idea of decoupling through indirection: nobody references concrete instances, just as in Factory Method nobody referenced concrete classes — here the "factory" manufactures addresses for you.
Circuit Breaker
The remote call introduces a new failure mode: the slow service. If Payments takes 30 seconds to respond, every checkout holds a thread for 30 seconds, threads run out, and Orders goes down too: cascading failure. The Circuit Breaker (popularized by Nussbaumer/Nygard in Release It!) acts like a fuse:
stateDiagram-v2
[*] --> Closed
Closed --> Open : failures >= threshold
Open --> HalfOpen : wait time elapses
HalfOpen --> Closed : probe call OK
HalfOpen --> Open : probe call fails
Closed --> Closed : success (resets counter)
Doesn't that state machine remind you of something? It is the State pattern from 04-09 applied to the health of a connection. A simplified implementation:
public class CircuitBreaker {
private enum State { CLOSED, OPEN, HALF_OPEN }
private State state = State.CLOSED;
private int consecutiveFailures = 0;
private long openSince = 0;
private final int failureThreshold; // e.g. 5
private final long waitMs; // e.g. 10_000
public CircuitBreaker(int failureThreshold, long waitMs) {
this.failureThreshold = failureThreshold;
this.waitMs = waitMs;
}
public synchronized <T> T execute(Supplier<T> call, Supplier<T> fallback) {
if (state == State.OPEN) {
if (System.currentTimeMillis() - openSince < waitMs) {
return fallback.get(); // circuit open: we don't even try
}
state = State.HALF_OPEN; // let ONE probe call through
}
try {
T result = call.get();
state = State.CLOSED; // success: the service has recovered
consecutiveFailures = 0;
return result;
} catch (Exception e) {
consecutiveFailures++;
if (state == State.HALF_OPEN || consecutiveFailures >= failureThreshold) {
state = State.OPEN;
openSince = System.currentTimeMillis();
}
return fallback.get();
}
}
}What to understand in this code: in OPEN we fail fast with a fallback (for instance, "we saved your order and will charge you in a few minutes") instead of waiting for a timeout; HALF_OPEN lets a single probe call through to check whether the service came back to life. In production you would use Resilience4j, which wraps the call exactly like a Decorator (03-05) — in fact its API is literally called CircuitBreaker.decorateSupplier.
Retry with backoff and idempotency
Many network failures are transient: retrying is usually enough. But retrying badly is worse than not retrying:
- Exponential backoff: wait 100 ms, then 200, 400, 800... so you don't finish off a service that is recovering. Add jitter (randomness) so a thousand clients don't retry in lockstep.
- Idempotency: if you retry
charge(order)because the response never arrived... what if the first charge did go through? Double charge. The solution is to make the operation idempotent: running it twice has the effect of running it once. In PideYa, every charge carries an idempotency key (the order id); if Payments receives the same key twice, the second time it returns the stored result without charging again.
Practical rule: never enable retries on a non-idempotent operation. This duo will reappear with messaging in 06-03.
Saga: transactions across services
In the monolith, checkout was a DB transaction: order + charge + stock, all or nothing (the Unit of Work from the previous lesson). With Database per Service there is no longer any transaction spanning Orders, Payments, and Delivery. The Saga pattern replaces it with a sequence of local transactions, where every failing step triggers compensations that undo the previous steps.
| Style | How it works | Reminds you of |
|---|---|---|
| Choreography | Each service listens for events and reacts; nobody directs | Chained Observer; simple with few steps, unreadable with many |
| Orchestration | A saga orchestrator tells each service what to do and awaits its reply | Mediator (04-06): it centralizes the conversation, like DispatchCenter |
Orchestrated saga for PideYa's checkout:
- Orchestrator → Orders: create order (local, committed).
- Orchestrator → Payments: charge. On failure → compensate: Orders.cancelOrder().
- Orchestrator → Delivery: assign courier. On failure → compensate: Payments.refund(), Orders.cancelOrder().
Compensations are the intent of Command's undo (04-03) at system scale: every step defines its inverse operation. Careful: it is not a magic rollback — the charge happened, and the refund is a new business operation, visible to the customer.
Strangler Fig: migrating the monolith
PideYa does not rewrite the monolith in one go (the "big bang" almost always ends badly). The Strangler Fig pattern (Fowler) — named after the strangler fig tree that grows around a host tree until it replaces it — consists of:
- Putting a routing facade (the API Gateway itself will do) in front of the monolith.
- Extracting one capability (e.g. Notifications, the most decoupled one thanks to the Bridge from 03-03) into a new service.
- Redirecting that traffic at the gateway to the new service; everything else keeps going to the monolith.
- Repeating until the monolith is empty... or until it stops hurting, which sometimes comes first.
It is the refactoring strategy from 05-04 raised to architecture: small steps, a system that never stops working, and the characterization tests are now contract tests between services.
Database per Service
Each service owns its database and nobody else touches it; everyone else goes through its API. Without this, microservices are a distributed monolith: the shared DB couples the schemas and the deployments.
Consequences you have to accept (they are not defects, they are the price):
- No JOINs across services: the "my orders with dish names" screen requires composing data from Orders and Catalog (in the gateway/BFF, or by duplicating data).
- No global transactions: hence the sagas.
- Eventual consistency: duplicated data takes time to converge — we will cover it in depth in the next lesson.
- In exchange: each service picks its own storage (Catalog can use a document store; Payments, strict relational) and deploys its schema without coordination.
Common Mistakes and Tips
- Starting with microservices: for a small team, the well-organized monolith of lesson 06-01 is faster and cheaper. Microservices solve problems of organizational scale (many teams, independent deployments). "Wait until it hurts" applies here too.
- The distributed monolith: services that share a DB or must be deployed together. You get all the costs of the network and none of the benefits. Symptom: a typical user story touches three services.
- Nanoservices: cutting too fine (one service per entity) multiplies network calls and sagas. The right cut follows the bounded contexts, not the tables.
- Retries without idempotency: the classic double charge. Design the idempotency key before enabling retries.
- Circuit breaker with no thought-out fallback: opening the circuit and returning a bare 500 error barely improves anything. The value is in the business plan B (degrade, enqueue, answer from cache).
- Saga without designed compensations: if
refund()doesn't exist or can fail with no plan, the saga leaves the system in an invisible intermediate state. Compensations are first-class business requirements.
Exercises
- Spot the bad cut. An architect proposes these services for PideYa:
entities-service(all domain classes),logic-service(all use cases), anddata-service(all DB access). Explain why this is a bad cut and which criterion should be used instead. - Trace the saga. The customer orders with a loyalty coupon (module from the case study in 05-02). The saga is: create order → redeem coupon (Loyalty service) → charge discounted amount → assign delivery. List the compensations required if (a) the charge fails and (b) the delivery assignment fails.
- Reason through the circuit breaker. With the lesson's
CircuitBreakerconfigured withfailureThreshold=3andwaitMs=10000: starting fromCLOSED, these calls arrive: failure, failure, success, failure, failure, failure, (4 s pass) a call, (7 more seconds pass) a successful call. State the state after each step and what each caller receives.
Solutions
- It is a cut along technical layers, not business capabilities: any new feature ("add tips") would touch all three services at once, forcing coordinated deployments — a distributed monolith. The right criterion is bounded contexts: boundaries within which a model is coherent and a team can work and deploy autonomously (catalog, orders, payments, delivery, notifications).
- (a) The charge fails: compensate in reverse order → Loyalty.returnCoupon() and Orders.cancelOrder(). (b) The delivery assignment fails: Payments.refund(), Loyalty.returnCoupon(), Orders.cancelOrder(). Notice that compensations run in the reverse order of the steps, exactly like a Command undo stack; and that "returning an already redeemed coupon" must exist as a business operation.
- Failure (1/3, CLOSED) → failure (2/3, CLOSED) → success (counter to 0, CLOSED) → failure (1/3) → failure (2/3) → failure (3/3 → OPEN, the instant is recorded). At 4 s: still OPEN (4000 < 10000), the caller receives the fallback without the call being attempted. At 11 s after opening: it moves to HALF_OPEN, the probe call runs, it succeeds → CLOSED and counter to 0; the caller receives the real result.
Conclusion
PideYa is now a platform: five services cut along bounded contexts, a gateway that hides them (Facade), a registry that locates them, fuses that isolate their failures (State), idempotent retries, and sagas that replace transactions (Mediator + Command's undo), all migrated gradually with Strangler Fig. But we have taken the most fragile part for granted: the communication itself. What happens when a message gets lost, duplicated, or arrives late? How does the "order confirmed" event reach five services without calling them one by one? That is the territory of queues, brokers, and eventual consistency: Design Patterns in Distributed Systems.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
