The microservices architecture consists of building an application as a set of small, autonomous, and independently deployable services, each responsible for a specific business capability and communicating through lightweight mechanisms such as HTTP APIs or messaging. It is one of the most popular architectural styles of the last decade, but also one of the most misunderstood: it brings enormous benefits when applied with judgment and a considerable cost in complexity when adopted because it is trendy.
In this lesson we will define its characteristics, compare it with the monolith, analyze when it is suitable (and when it is not), and see a concrete example of decomposing an insurance application.
Contents
- Definition and characteristics
- Microservices versus monolith
- The modular monolith as an alternative
- When to USE microservices
- When NOT to use microservices
- Decomposition example
- Common mistakes and tips
- Exercises
- Conclusion
- Definition and characteristics
A microservice is a service that ideally meets these characteristics:
- Single business responsibility: it manages a specific capability (policies, claims, billing).
- Independent deployment: it can be deployed without coordinating with the rest.
- Its own database: each service owns its data; no one accesses its database directly.
- Team autonomy: one team develops, tests, and operates it from end to end.
- Communication over the network: via API or messages, never by sharing memory.
- Fault tolerance: designed to withstand its dependencies failing.
graph TB
subgraph "Insurance Application"
P[Policies Service]
S[Claims Service]
C[Customers Service]
F[Billing Service]
end
P --> DBP[(Policies DB)]
S --> DBS[(Claims DB)]
C --> DBC[(Customers DB)]
F --> DBF[(Billing DB)]Notice that each service has its own database. This is one of the most important principles: a service never reads from or writes to another's database. If it needs data belonging to another, it requests it via API or reacts to events.
- Microservices versus monolith
A monolith is an application deployed as a single unit. It is not inherently bad; in fact, it is usually the right starting point. The honest comparison is:
| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment | One unit | Many independent units |
| Scaling | All or nothing | Per service, as needed |
| Operational complexity | Low | High (network, observability, orchestration) |
| Initial velocity | High | Low (a lot of upfront infrastructure) |
| Fault isolation | Poor (one failure can bring everything down) | Good (contained failures) |
| Transactions | Simple local ACID | Distributed and complex (sagas) |
| Technology | A common stack | Heterogeneous per service |
| Team autonomy | Low (everyone touches the same thing) | High |
| Debugging | Simple (one process, one trace) | Complex (distributed traces) |
The key takeaway: microservices trade development complexity for operational complexity. They do not eliminate it.
- The modular monolith as an alternative
Before jumping to microservices, there is a very underrated intermediate option: the modular monolith. It is a single deployable, but internally organized into modules with clear boundaries and explicit communication between them.
// Well-separated modules within a single deployable.
// The claims module does NOT access the internal classes of policies;
// it only uses its public API (an interface).
package com.fiatc.claims;
public class ClaimsService {
private final PolicyApi policyApi; // public interface of the Policies module
public ClaimsService(PolicyApi policyApi) {
this.policyApi = policyApi;
}
public void registerClaim(String policyId) {
// We query through the public API, not internal tables
if (!policyApi.isActive(policyId)) {
throw new IllegalStateException("The policy is not active");
}
// ... registration logic
}
}This approach gives you clean boundaries without the cost of the network. If later a module needs to scale or be deployed separately, it is already isolated and extracting it into a microservice is much easier. It is the recommended strategy to start with.
- When to USE microservices
Microservices make sense when several of these conditions are met:
- The organization is large: many teams stepping on each other while working on the same code.
- Disparate scaling needs: the billing service needs 20 instances and the configuration one needs only 1.
- Different rates of change: some parts change daily and others almost never.
- Availability requirements per domain: isolating a failure in claims so it doesn't bring down policy sales.
- DevOps maturity: there is CI/CD automation, containers, observability, and orchestration.
The famous "Conway's law" sums it up: the architecture of the system tends to reflect the communication structure of the organization. If you have autonomous teams, microservices fit.
- When NOT to use microservices
Avoid microservices if:
- The team is small: the operational overhead is not worth it.
- The domain is not clear: if you still don't understand the boundaries well, you will draw them wrong and pay a very high cost to correct them.
- There is no operational maturity: without observability or automation, debugging is a nightmare.
- You seek extreme transaction performance: distributed transactions are slow and complex.
A common antipattern is the distributed monolith: microservices so coupled to each other that they must be deployed together. It combines the worst of both worlds: the complexity of the network and the rigidity of the monolith.
| Symptom | Diagnosis |
|---|---|
| Deploying one service requires deploying others | Distributed monolith |
| A schema change affects several services | Poorly defined boundaries |
| Services that share a database | Data coupling |
| Long chains of synchronous calls | Temporal coupling |
- Decomposition example
Let's start from an insurance monolith with everything in a single process: customers, policies, claims, and billing. A reasonable decomposition, by business capabilities, would be:
# Resulting service map
services:
- name: customers
responsibility: "Contact details and profile of the insured"
owned_data: [customer, address]
- name: policies
responsibility: "Underwriting and policy lifecycle"
owned_data: [policy, coverage]
- name: claims
responsibility: "Opening and handling of claims"
owned_data: [claim, appraisal]
- name: billing
responsibility: "Receipts, charges, and refunds"
owned_data: [receipt, charge]Each service owns its tables. How does the claims service get the customer's name? It does not access the customers database; it requests it via API or maintains a local copy of the few data items it needs, updated through events:
sequenceDiagram
participant CL as Customers Service
participant BUS as Event bus
participant SI as Claims Service
CL->>BUS: CustomerUpdated event
BUS->>SI: CustomerUpdated
Note over SI: Updates its local copy<br/>(name, anonymized tax ID)When a customer changes their data, the customers service publishes an event. The claims service keeps a minimal copy of what it needs and updates it upon receiving the event. This avoids direct coupling to another's database.
Common Mistakes and Tips
- Starting directly with microservices: prefer a modular monolith until you understand the domain well.
- Sharing a database between services: breaks autonomy and creates hidden coupling. Each service, its own database.
- Making microservices too small (nanoservices): they generate an explosion of network calls and make it harder to reason about the system.
- Forgetting observability: invest from day one in distributed traces, metrics, and correlated logs.
- Synchronizing everything: long chains of synchronous calls couple availability. Consider asynchronous events.
Exercises
- Build a table with three advantages and three drawbacks of microservices versus the monolith, justifying each point.
- A startup of 4 developers wants to launch an MVP in 3 months. Would you recommend microservices? Reason your answer and propose an alternative.
- Given an e-commerce monolith with catalog, cart, orders, and payments modules, propose a decomposition into services indicating what data each one owns and how the orders service gets the current price of a product.
Solutions
-
Advantages: independent deployment (less coordination), selective scaling (resource savings), fault isolation (higher availability). Drawbacks: operational complexity (network, orchestration), distributed transactions (sagas), harder debugging (distributed traces).
-
No. With 4 people and pressure to validate the product, the operational overhead of microservices would delay the MVP. Recommendation: a modular monolith with clean boundaries, which allows extracting services later if the product grows.
-
Catalog owns
productandprice; cart ownscart_line; orders ownsorderandorder_line; payments ownstransaction. The orders service gets the price by calling the catalog API at the moment of confirming the order and, very importantly, stores the applied price in its ownorder_linetable, because the catalog price may change afterwards.
Conclusion
Microservices are small, autonomous, and independently deployable services, each owning its data. They trade development complexity for operational complexity, so they are only worthwhile when the organization, the scaling, and the technical maturity justify it. When in doubt, the modular monolith is an excellent starting point.
The hardest question that remains open is where to draw the boundaries between services. We will dedicate the next lesson, Service Decomposition and Bounded Contexts, to this, where we will apply the ideas of Domain-Driven Design to get the size and limits of each service right.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
