25.3 Microservices
Table of Contents
Why Microservices Exist
Monoliths, modular monoliths, and microservices all aim to solve the same problem: build and run a useful application. Microservices appear when a single deployable application starts to become too big, too slow to change, or too hard to understand.
Common pain points that push teams toward microservices:
- The codebase is so large that:
- Build times are very long.
- A small change requires understanding many unrelated parts.
- Deployments are risky:
- One bug can break the entire application.
- You cannot deploy part of the system independently.
- Teams are blocked:
- Multiple teams constantly merge into the same large repo.
- Changes from one team conflict with others.
Microservices try to solve these issues by splitting the system into many small, independently deployable services. Each service owns a specific part of the domain, for example:
- User service
- Orders service
- Payments service
- Inventory service
Each service:
- Has its own codebase.
- Can be deployed independently.
- Communicates with other services via network calls, usually HTTP or messaging.
Microservices are not automatically “better.”
They introduce network complexity, data consistency issues, and operational overhead. Only consider them when a simpler architecture, such as a modular monolith, is not enough.
Characteristics of Microservices
Independent deployability
The core property of microservices is independent deployability.
Each microservice:
- Can be changed, tested, and deployed without redeploying other services.
- Has its own lifecycle: versioning, rollout, rollback.
Example:
- You have:
users-serviceat version 1.4.0.orders-serviceat version 2.1.3.payments-serviceat version 0.9.0.- You can deploy a new version of
payments-servicealone, as long as its API contract remains compatible.
This is different from a monolith, where all modules compile into one application and must be deployed together.
Service boundaries around business capabilities
Microservices are usually organized around business capabilities, not technical layers.
Examples of business capabilities:
| Capability | Possible Microservice |
|---|---|
| User accounts | user-service |
| Product catalog | catalog-service |
| Orders | order-service |
| Payments | payment-service |
| Shipping | shipping-service |
Each service:
- Contains all layers it needs: HTTP handlers, business logic, database access.
- Does not expose its internal database to other services.
- Communicates using APIs or messages only.
Technology and data independence
In a microservices system:
- Each service can use a different tech stack:
user-servicewritten in Python with FastAPI and PostgreSQL.payment-servicewritten in Go with MySQL.- Each service can have its own database schema and even a different database type:
order-servicewith PostgreSQL.search-servicewith Elasticsearch.analytics-servicewith a columnar database.
This is known as polyglot persistence.
Rule: “Database per service”
Each microservice should have its own database that is not shared with other services. External access to another service’s database is forbidden. Communication must go through service APIs or events, not direct DB queries.
Small and focused services
There is no strict size limit, but typical guidelines:
- A service should be small enough that:
- A small team (2 to 8 people) can own and understand it.
- You can rewrite it from scratch in weeks, not years.
- A service should do one thing well.
Examples:
- Good service boundaries:
auth-service: handles login, registration, token issuing.notification-service: sends emails, SMS, push notifications.- Bad boundaries:
utilities-service: contains random helper functions.monolith-service: a “service” that does everything.
Example: Microservices in an E‑Commerce System
Consider an e-commerce backend. In a monolithic design, you might have:
- Single app:
ecommerce-app - Database with tables:
users,products,orders,payments,inventory,shipments.
In a microservices design, you might have:
| Service | Responsibilities | Own Database Example |
|---|---|---|
user-service | User accounts, profiles, auth | users_db (PostgreSQL) |
catalog-service | Products, categories, search indexing | catalog_db (PostgreSQL) |
inventory-service | Stock levels, reservations | inventory_db (Redis + Postgres) |
order-service | Creating and managing orders, order history | orders_db (PostgreSQL) |
payment-service | Payment processing, refunds | payments_db (PostgreSQL) |
shipping-service | Shipments, tracking numbers, shipping rates | shipping_db (PostgreSQL) |
notification-service | Order confirmation emails, password reset mails | notifications_db (MongoDB) |
A typical flow when a customer places an order:
- Client calls
order-service: POST /ordersorder-service:- Calls
user-serviceto verify user ID and address. - Calls
catalog-serviceto confirm product prices. - Calls
inventory-serviceto reserve stock. - Calls
payment-serviceto charge the customer. order-servicecreates an order in its own database.order-servicepublishes anOrderCreatedevent.notification-servicelistens toOrderCreatedand sends a confirmation email.shipping-servicelistens toOrderCreatedand creates a shipment.
Notice that no service reads from another service’s database. They all use APIs or messages.
Synchronous vs Asynchronous Communication Between Services
Microservices communicate over the network. There are two main styles.
Synchronous communication
This is usually HTTP or gRPC calls where a service waits for a response.
Example:
order-servicemakes an HTTP request topayment-service:
POST /payments
Content-Type: application/json
{
"order_id": "ord_123",
"amount": 49.99,
"currency": "USD",
"user_id": "user_456"
}
payment-service responds:
201 Created
Content-Type: application/json
{
"payment_id": "pay_789",
"status": "completed"
}Pros:
- Simple to understand and debug.
- Fits well with request-response patterns.
Cons:
- Tightly couples performance and availability:
- If
payment-serviceis slow,order-serviceis slow. - If
payment-serviceis down,order-servicemight fail.
Asynchronous communication
Services use a message broker or event bus, such as:
- RabbitMQ
- Kafka
- Redis streams
- AWS SQS
Example:
order-servicepublishes anOrderCreatedevent to a queue or topic:
{
"event_type": "OrderCreated",
"order_id": "ord_123",
"user_id": "user_456",
"total": 49.99
}notification-servicesubscribes to this event and sends an email.shipping-servicesubscribes and creates a shipment.
Pros:
- Services are decoupled in time:
- If
notification-serviceis down, events are queued. - Great for background work, retries, and eventual consistency.
Cons:
- Harder to debug, since flows are distributed and delayed.
- Requires careful design to ensure messages are not lost or duplicated.
A real microservices system usually uses both:
- Synchronous calls for critical validations or user-facing flows.
- Asynchronous events for side effects such as emails, analytics, indexing.
Data Management in Microservices
Database per service
As stated before, each service owns its data. This has several consequences:
- No cross-service joins at the database level.
- Queries that need data from multiple services must be done:
- By calling multiple services.
- Or by maintaining a denormalized read model.
Example limitation:
- You cannot write SQL like:
SELECT u.email, o.total
FROM users u
JOIN orders o ON u.id = o.user_id;
if users and orders are in different databases.
Instead, you might:
- Have an
analytics-servicethat: - Listens to
UserRegisteredandOrderCreatedevents. - Builds its own
user_orderstable optimized for reporting.
Eventual consistency
In a monolith with a single database, you can often wrap everything in one transaction and be sure that all related changes commit together.
In microservices, with separate databases and network calls, this is not always possible. Instead, you often use eventual consistency.
Definition:
- The system may be temporarily inconsistent.
- After some time, when all messages and updates are processed, data across services becomes consistent.
Example:
order-servicecreates an order and emitsOrderCreated.inventory-servicereceives the event and lowers stock.- For a short time, another service might read:
- The new order exists.
- But inventory still shows the old stock.
- After inventory updates, everything aligns.
Important consistency rule
In microservices, you must design for inconsistencies over short periods of time and avoid assumptions that all parts of the system see the same data instantly. Use patterns like idempotent operations, retries, and compensating actions.
Distributed transactions and sagas
Traditional ACID transactions across multiple databases are complex and often avoided in microservices.
Instead, use sagas, which are sequences of local transactions with compensating actions.
Example: Order creation saga
order-servicecreates an order with statuspending.order-servicerequests:inventory-serviceto reserve stock.payment-serviceto charge the customer.- If both succeed:
order-servicesets status toconfirmed.- If payment fails:
order-serviceasksinventory-serviceto release the reserved stock.order-servicesets status tocancelled.
Here, the “compensating action” for reserving stock is releasing stock.
Sagas can be:
- Orchestrated: A central service (e.g.
order-service) decides the next step. - Choreographed: Services react to events from others.
Microservice API Design and Versioning
Because services communicate via APIs, contracts between them are critical.
API contracts
You should treat inter-service APIs as stable contracts:
- Define them clearly:
- HTTP paths
- Request and response JSON formats
- HTTP status codes
- Use documentation and schemas, such as:
- OpenAPI for HTTP APIs.
- Protobuf schemas for gRPC.
Example order-service API:
POST /orders
Content-Type: application/json
{
"user_id": "user_123",
"items": [
{"product_id": "prod_1", "quantity": 2},
{"product_id": "prod_2", "quantity": 1}
]
}Response:
201 Created
Content-Type: application/json
{
"order_id": "ord_987",
"status": "pending_payment"
}Other services rely on this contract. Changing it can break them.
Backward compatibility and versioning
Because services are deployed independently, you must support backward compatible changes where possible:
- Safe changes:
- Adding optional fields.
- Adding new endpoints.
- Dangerous changes:
- Renaming or removing fields.
- Changing field types.
Versioning approaches:
- URL versioning:
/v1/orders,/v2/orders - Header versioning:
X-API-Version: 1 - Semantic versioning in documentation and client libraries.
Example:
order-servicev1 uses:
{
"order_id": "ord_987",
"status": "pending_payment"
}- Later you add:
{
"shipment_estimate": "2026-09-01"
}If you only add this field, existing clients are not broken.
API rule
When multiple services depend on an API, treat it as a public contract. Avoid breaking changes, or provide a clear versioning strategy and migration period.
Operational Challenges in Microservices
Microservices trade in-process calls for network calls. This creates new challenges.
Observability
With many services, you need strong observability:
- Logging:
- Structured logs (JSON).
- Correlation or trace IDs passed across services.
- Metrics:
- Request counts, latency, error rates per service and endpoint.
- Tracing:
- Distributed tracing to follow a request through multiple services.
Example:
- A user action triggers calls across:
api-gateway→auth-service→order-service→payment-service.- With tracing, you can see the timeline and where delays occur.
Network reliability
In microservices, failures are common:
- A service can be:
- Down.
- Slow.
- Overloaded.
You must design for this with:
- Timeouts:
- Never let a request wait forever.
- Retries:
- Retry on transient errors, such as timeouts or rate limits.
- Backoff strategies:
- Increase delay between retries, for example exponential backoff.
- Circuit breakers:
- Stop calling a failing service for a period to avoid cascading failures.
Example of a simple retry strategy:
- Call
payment-service. - If timeout:
- Wait 200 ms, retry.
- If fails again, wait 400 ms, retry.
- If fails again, mark as failure.
Service discovery
In a dynamic environment, services often:
- Scale horizontally, with many instances.
- Move between machines or containers.
You need a way to find where each service lives:
- Service registry:
- Such as Consul, Eureka, Kubernetes services.
- API gateway:
- A single entry point that knows how to route to internal services.
Example:
order-servicealways callshttp://payment-service/payments.- In Kubernetes,
payment-serviceis a DNS name that resolves to the current pods.
Configuration management
Each service has:
- Environment variables.
- Database connection strings.
- Secret keys.
- Feature flags.
Microservices make configuration management more complex because of the number of services. Common approaches:
- Use environment variables for each container.
- Use centralized configuration services.
- Use secret managers for credentials.
Microservices vs Modular Monolith
Because this course also covers modular monoliths, it is important to compare.
| Aspect | Modular Monolith | Microservices |
|---|---|---|
| Deployment unit | Single application | Many services |
| Communication | In-process function calls | Network calls (HTTP, gRPC, messaging) |
| Databases | Often shared DB | Database per service |
| Transactions | Local, simple ACID | Distributed, sagas, eventual consistency |
| Complexity | Lower operational complexity | Higher operational complexity |
| Performance | No network latency between modules | Network latency and overhead |
| Team independence | Some independence, but shared deploys | High independence, separate deployments |
| Best for | Small to medium systems, early-stage products | Large, complex systems and many teams |
Guideline
Start with a modular monolith. Move to microservices only when:
- You have clear module boundaries.
- You face real problems that microservices can solve, such as:
- Independent team deployments.
- Different scalability needs for different modules.
When Microservices Make Sense
Microservices are helpful when:
- You have many teams working on different parts of the system, and:
- They often block each other in the monolith.
- They need independent release cycles.
- Some parts of the system need to scale differently:
search-serviceorvideo-processing-serviceneed much more CPU.auth-serviceis mostly idle.- You need strong fault isolation:
- You want a failure in
notification-servicenot to take down order processing. - You need technological flexibility:
- Some parts require a different language or specialized database.
Examples:
- Large e-commerce platforms.
- Streaming platforms.
- Large SaaS products with many independent feature teams.
Common Pitfalls of Microservices
Microservices are often misused. Typical mistakes include:
Splitting too early
Teams adopt microservices:
- Before they understand the domain well.
- Before they build a solid modular monolith.
Result:
- Wrong service boundaries.
- High cross-service chatter.
- Difficulty in changing business logic.
Too many tiny services
An extreme “nano-service” approach creates:
- Dozens or hundreds of very small services.
- High operational overhead.
- Complex deployment and debugging.
Better:
- Services that are small but meaningful around business capabilities.
Shared database
A very common anti-pattern:
- Multiple services share the same database schema.
- Services read each other’s tables.
This leads back to monolithic coupling, but with added network and deployment complexity.
Ignoring consistency and failures
Treating microservices as if they were function calls leads to:
- Assumptions of instant consistency.
- No handling for partial failures.
- User-facing confusion like:
- “You paid, but your order status is still pending.”
You must design for:
- Retries.
- Idempotent operations.
- Compensating actions.
Simple Microservices Example with HTTP
Consider two simple FastAPI services: user-service and order-service.
user-service:
# user_service/main.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
fake_users = {
"user_1": {"id": "user_1", "email": "alice@example.com"},
"user_2": {"id": "user_2", "email": "bob@example.com"},
}
@app.get("/users/{user_id}")
def get_user(user_id: str):
user = fake_users.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
order-service:
# order_service/main.py
from fastapi import FastAPI, HTTPException
import httpx
app = FastAPI()
fake_orders = {}
USER_SERVICE_URL = "http://user-service:8000"
@app.post("/orders")
async def create_order(user_id: str):
async with httpx.AsyncClient() as client:
try:
resp = await client.get(f"{USER_SERVICE_URL}/users/{user_id}", timeout=2.0)
except httpx.RequestError:
raise HTTPException(status_code=503, detail="User service unavailable")
if resp.status_code == 404:
raise HTTPException(status_code=400, detail="Invalid user_id")
if resp.status_code != 200:
raise HTTPException(status_code=502, detail="Error calling user service")
order_id = f"ord_{len(fake_orders) + 1}"
fake_orders[order_id] = {"id": order_id, "user_id": user_id, "status": "created"}
return fake_orders[order_id]This example shows:
- Services as separate applications.
- Synchronous HTTP call from
order-servicetouser-service. - Timeouts and error handling.
- A simple API contract between services.
Summary
Microservices are an architectural style where:
- You build a system from multiple small, independent services.
- Each service owns its own data and domain.
- Services communicate over the network, synchronously or asynchronously.
- You trade code-level simplicity and strong consistency for:
- Team independence.
- Independent deployment.
- Scalability and fault isolation.
As a backend developer, you should:
- Understand how microservices differ from monoliths and modular monoliths.
- Know the basic patterns:
- Database per service.
- Event-driven communication.
- Sagas and eventual consistency.
- Be aware of the operational and conceptual complexity they introduce.
In later chapters, concepts like service layers, repository pattern, and event-driven architecture will connect directly to these microservice ideas and show how to use them in practice.
Views: 6
KAHIBARO