Microservices Architecture
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Why Microservices Now
From blueprint to independently moving parts
In High-Level System Design, you learned to draw system boundaries and data flow. In Low-Level Design Principles, you practiced turning responsibilities into classes and interfaces.
Now we zoom out one level: what if some responsibilities become separate deployable applications?
That is the core idea of microservices.
A microservices architecture helps when one large application becomes hard to change, test, deploy, or scale as a single unit.
In this lesson, we will learn
- What a microservice is — and what it is not
- How to choose service boundaries
- How services communicate
- Why resilience matters when calls cross the network
- How to reason about microservices using Java-friendly examples
Think of this lesson as the bridge between HLD diagrams and real production systems.

Microservice Mental Model
A microservice is a small product, not just a small class
A microservice is an independently deployable service that owns a focused business capability.
For example, in an e-commerce system:
Client App
|
v
API Gateway
|
+--> User Service
+--> Catalog Service
+--> Order Service
+--> Payment Service
+--> Inventory Service
Each service usually has: - A clear responsibility - Its own API - Its own release cycle - Its own runtime process - Its own data ownership boundary
Not every split is a microservice
If you split code into many folders but deploy everything together, that is modular code — useful, but not microservices.
If each part can be built, deployed, monitored, and scaled independently, you are closer to microservices.

Choosing Boundaries
Good boundaries follow business capabilities
A common beginner mistake is splitting services by technical layer:
Controller Service
Business Logic Service
Database Service
That usually creates chatty, fragile systems.
A better split follows business capabilities:
Order Service -> manages order lifecycle
Payment Service -> authorizes and records payments
Inventory Service -> reserves and releases stock
User Service -> manages customer identity/profile
This connects directly to what you practiced in Low-Level Design Principles: responsibility matters. The difference is that now the responsibility boundary may become a process boundary.
Boundary checklist
Ask: - Can this capability change independently? - Does one team own it naturally? - Does it have clear inputs and outputs? - Would scaling it separately be useful? - Can it fail without taking everything down?
If most answers are yes, it may be a service candidate.

Boundary Check
You are designing an e-commerce platform. Which split is usually the strongest microservice boundary?
Service Communication
Services talk through contracts
Once services become separate applications, method calls become network calls.
Two common styles are:
1. Synchronous request/response
One service calls another and waits.
Example: Order Service asks Payment Service, “Can you charge this card?”
2. Asynchronous events
One service publishes an event and continues.
Example: Order Service publishes OrderCreated, and Inventory Service reacts later.
Use synchronous calls when you need an immediate answer. Use events when work can happen later and you want looser coupling.
The code below simulates both styles without needing Spring Boot, HTTP, or a broker.
```java
import java.util.*;
import java.util.concurrent.*;
public class Main {
interface PaymentService {
boolean charge(String orderId, int amountCents);
}
static class MockPaymentService implements PaymentService {
public boolean charge(String orderId, int amountCents) {
System.out.println("PaymentService: charging " + amountCents + " cents for " + orderId);
return true;
}
}
static class EventBus {
private final Queue<String> events = new ConcurrentLinkedQueue<>();
void publish(String event) {
events.add(event);
System.out.println("EventBus: published " + event);
}
void processEvents() {
while (!events.isEmpty()) {
String event = events.poll();
System.out.println("InventoryService: reacting to " + event);
}
}
}
static class OrderService {
private final PaymentService paymentService;
private final EventBus eventBus;
OrderService(PaymentService paymentService, EventBus eventBus) {
this.paymentService = paymentService;
this.eventBus = eventBus;
}
void placeOrder(String orderId, int amountCents) {
System.out.println("OrderService: received order " + orderId);
boolean paid = paymentService.charge(orderId, amountCents); // synchronous
if (!paid) {
System.out.println("OrderService: payment failed");
return;
}
eventBus.publish("OrderCreated:" + orderId); // asynchronous style
System.out.println("OrderService: order accepted");
}
}
public static void main(String[] args) {
EventBus eventBus = new EventBus();
OrderService orderService = new OrderService(new MockPaymentService(), eventBus);
orderService.placeOrder("ORD-101", 4999);
eventBus.processEvents();
}
}
```
Data Ownership
True or false: In a healthy microservices architecture, it is usually fine for the Order Service to directly read and write the Payment Service database tables whenever it needs payment details.
Failure Is Normal
The network is now part of your design
Inside a monolith, a method call is usually fast and local.
Across microservices, a call can fail because: - The target service is down - The network is slow - A timeout occurs - A deployment is in progress - A dependency is overloaded
So microservices require resilience patterns.
One common pattern is a circuit breaker. It stops calling a failing dependency for a short period, giving the system time to recover instead of repeatedly making doomed calls.
The simplified Java example below shows the idea.
```java
import java.util.function.Supplier;
public class Main {
static class CircuitBreaker {
private int failures = 0;
private final int failureLimit;
private boolean open = false;
CircuitBreaker(int failureLimit) {
this.failureLimit = failureLimit;
}
String call(Supplier<String> remoteCall) {
if (open) {
return "Circuit open: using fallback response";
}
try {
String result = remoteCall.get();
failures = 0;
return result;
} catch (RuntimeException ex) {
failures++;
if (failures >= failureLimit) {
open = true;
}
return "Call failed: " + ex.getMessage();
}
}
}
public static void main(String[] args) {
CircuitBreaker breaker = new CircuitBreaker(2);
Supplier<String> failingPaymentService = () -> {
throw new RuntimeException("Payment Service timeout");
};
System.out.println(breaker.call(failingPaymentService));
System.out.println(breaker.call(failingPaymentService));
System.out.println(breaker.call(failingPaymentService));
}
}
```
Front Door Blank
In many microservices systems, clients call one public front door that routes requests to internal services. Fill in the blank:
The common name for this front door is an ____.
Tiny Gateway Demo
A gateway hides internal service locations
Clients should not need to know every internal service address.
Instead, a gateway can route requests based on path:
/api/orders -> Order Service
/api/payments -> Payment Service
/api/users -> User Service
In Spring Boot, this could be implemented with Spring Cloud Gateway or a controller-based edge service. Here is a plain Java simulation that keeps the architecture idea visible.
```java
import java.util.*;
public class Main {
interface Microservice {
String handle(String request);
}
static class OrderService implements Microservice {
public String handle(String request) {
return "OrderService handled: " + request;
}
}
static class PaymentService implements Microservice {
public String handle(String request) {
return "PaymentService handled: " + request;
}
}
static class ApiGateway {
private final Map<String, Microservice> routes = new HashMap<>();
void register(String pathPrefix, Microservice service) {
routes.put(pathPrefix, service);
}
String route(String path, String request) {
for (Map.Entry<String, Microservice> entry : routes.entrySet()) {
if (path.startsWith(entry.getKey())) {
return entry.getValue().handle(request);
}
}
return "404 Not Found for path: " + path;
}
}
public static void main(String[] args) {
ApiGateway gateway = new ApiGateway();
gateway.register("/api/orders", new OrderService());
gateway.register("/api/payments", new PaymentService());
System.out.println(gateway.route("/api/orders/101", "GET order 101"));
System.out.println(gateway.route("/api/payments/charge", "POST charge card"));
System.out.println(gateway.route("/api/products", "GET products"));
}
}
```
Microservices Checklist
What to remember
Microservices are not automatically better than monoliths. They trade code simplicity for independent deployment, scaling, team ownership, and fault isolation.
Use microservices when the benefits justify the operational cost.
Before splitting a system, ask
- Is the boundary a real business capability?
- Can the service own its API clearly?
- Can it be deployed independently?
- Can it fail without collapsing the whole product?
- Do we have enough monitoring and logging to debug it?
- Are we avoiding direct access to another service’s private data?
Narrative checkpoint
You now have the architectural language to discuss services as deployable units. Next, we will focus on Data Design and Database Management, where service boundaries meet tables, indexes, relationships, and data ownership.
