Microservices Architecture in System Design
Microservices architecture builds an application as a collection of small, independent services, each responsible for one business capability, each with its own data, and each deployable on its own. Services cooperate over the network through APIs and events. This lets many teams build, release and scale their part of a large system without waiting for each other.
This tutorial explains the defining characteristics of microservices, the building blocks of a microservices system, how services communicate synchronously and asynchronously, how data is managed when each service owns its database, how to design for failure, and how to build services with Spring Boot. It also covers how to split a system into services, deployment and observability, advantages, challenges and common mistakes.
1. What Is Microservices Architecture?
Imagine a large restaurant. In a small café, one cook does everything. In a big restaurant kitchen, work is split into stations: grill, pastry, salads and drinks. Each station has its own staff, ingredients and equipment, and can be busy or quiet independently. If the pastry chef is off sick, the grill keeps working. The head waiter coordinates orders between stations. That kitchen is organised like a microservices system.
In software, a microservice is a small application that:
- does one business job well, such as orders, payments or notifications,
- owns its data and does not share its database with other services,
- exposes a clear API (REST, gRPC) or publishes events,
- can be built, tested, deployed and scaled independently,
- is usually owned by one small team from development to production.
"Micro" refers to the narrow scope of responsibility, not to a number of lines of code. A good service is small enough for one team to understand fully, and large enough that it does not need to call other services for every simple task.
2. Key Characteristics of Microservices
| Characteristic | Meaning |
|---|---|
| Organised around business capabilities | Services map to business areas (catalogue, checkout, shipping), not technical layers (UI, logic, database) |
| Independently deployable | A team can release its service many times a day without coordinating with other teams |
| Decentralised data | Each service has its own database or schema; others access the data only through the service's API or events |
| Smart endpoints, simple pipes | Business logic lives inside services; the network and message brokers just move messages |
| Technology freedom | Each service can choose the language, framework and database that fit its job, within organisation guidelines |
| Designed for failure | Services expect their dependencies to be slow or unavailable and handle it gracefully |
| Automated delivery | Builds, tests, deployments and infrastructure are automated, because there are many services to manage |
| Team ownership | "You build it, you run it": the team that writes the service also operates it |
3. Building Blocks of a Microservices System
A production microservices system needs more than the services themselves. Supporting components handle routing, discovery, messaging and visibility:
- API gateway: the single entry point for clients. It routes requests, checks authentication, applies rate limits and can combine responses. See API Gateway.
- Services: independently deployable applications, each with its own database.
- Service discovery: keeps track of where service instances are running, because instances start, stop and move constantly. Kubernetes services, Consul and Eureka are common options.
- Configuration management: central, versioned configuration for every service and environment.
- Message broker: carries events and commands asynchronously, decoupling services in time.
- Observability stack: central logging, metrics and distributed tracing, so you can follow one request across many services.
- Container platform and CI/CD: usually containers on Kubernetes or a managed platform, with one automated pipeline per service.
4. How Microservices Communicate
Services talk to each other in two styles, and most systems use both:
| Aspect | Synchronous (REST, gRPC) | Asynchronous (events, messages) |
|---|---|---|
| How it works | Caller sends a request and waits for the reply | Producer publishes a message; consumers process it later |
| Coupling | Caller needs the callee to be up right now | Producer and consumers are decoupled in time |
| Latency | Adds up along the call chain | Producer returns immediately; work completes later |
| Consistency | Immediate answer | Eventual: consumers catch up after a delay |
| Complexity | Simple to understand and debug | Needs a broker, retries, idempotent consumers and monitoring of lag |
| Best for | Queries that need an answer now, such as "get product price" | Notifications, workflows, updating other services, analytics |
A good rule: use synchronous calls for queries where the user needs an answer immediately, and asynchronous events for side effects that other services must react to. Avoid long chains of synchronous calls. Read more in Asynchronous Communication.
5. Data Management: Database per Service
The most important, and most difficult, rule of microservices is that each service owns its data. Other services may not read or write its tables directly; they must use its API or listen to its events. This keeps services independently changeable: the inventory team can redesign its schema without breaking the order team.
Owning data separately creates new problems, each with a standard solution:
- Transactions across services: you can no longer wrap "create order, reserve stock, charge card" in one database transaction. Use the Saga pattern: a sequence of local transactions with compensating actions if a later step fails.
- Reliable event publishing: saving to the database and publishing an event must not get out of step. The transactional outbox pattern writes the event to an outbox table in the same local transaction, and a separate process publishes it.
- Queries across services: a screen that shows orders with customer names cannot join two databases. Services keep local copies of the data they need (updated by events), or a separate read model is built for queries (CQRS).
- Consistency: data across services becomes eventually consistent. Design the user experience to tolerate short delays. See Consistency.
6. Building Microservices with Spring Boot
Spring Boot is one of the most popular ways to build microservices in Java. Below, the order service calls the inventory service synchronously, with timeouts and a circuit breaker, and publishes an event for everything else.
1. An HTTP client with strict timeouts. Without timeouts, one slow dependency can tie up all your threads:
package com.javabytechie.orders.config;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
@Configuration
public class HttpClientConfig {
@Bean
RestClient inventoryRestClient(RestClient.Builder builder) {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(1)); // fail fast if the service is unreachable
factory.setReadTimeout(Duration.ofSeconds(2)); // never wait forever for an answer
return builder.baseUrl("http://inventory-service").requestFactory(factory).build();
}
}
2. A client protected by a circuit breaker (using the Resilience4j Spring Boot starter and Spring AOP). When the inventory service keeps failing, the breaker opens and the fallback answers instantly instead of waiting on a broken service:
package com.javabytechie.orders.inventory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@Component
public class InventoryClient {
public record StockLevel(String sku, int available, boolean known) {
static StockLevel unknown(String sku) {
return new StockLevel(sku, 0, false);
}
}
private final RestClient inventory;
public InventoryClient(RestClient inventoryRestClient) {
this.inventory = inventoryRestClient;
}
@CircuitBreaker(name = "inventory", fallbackMethod = "stockUnknown")
public StockLevel stockFor(String sku) {
return inventory.get()
.uri("/api/stock/{sku}", sku)
.retrieve()
.body(StockLevel.class);
}
// Called when the call fails or the circuit is open: degrade gracefully instead of failing the page
private StockLevel stockUnknown(String sku, Throwable cause) {
return StockLevel.unknown(sku);
}
}
spring:
application:
name: order-service
resilience4j:
circuitbreaker:
instances:
inventory:
sliding-window-size: 20 # look at the last 20 calls
failure-rate-threshold: 50 # open when half of them fail
wait-duration-in-open-state: 30s # then fail fast for 30 seconds
permitted-number-of-calls-in-half-open-state: 3
3. Publishing and consuming events with Spring for Apache Kafka. The order service announces what happened; the notification service reacts in its own time:
package com.javabytechie.orders.events;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class OrderEventPublisher {
public record OrderPlaced(String orderId, String customerEmail, long totalInPaise) { }
private final KafkaTemplate<String, OrderPlaced> kafka;
public OrderEventPublisher(KafkaTemplate<String, OrderPlaced> kafka) {
this.kafka = kafka;
}
public void orderPlaced(OrderPlaced event) {
kafka.send("order-events", event.orderId(), event); // key by order id keeps one order's events in order
}
}
package com.javabytechie.notifications;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class OrderEventsListener {
public record OrderPlaced(String orderId, String customerEmail, long totalInPaise) { }
@KafkaListener(topics = "order-events", groupId = "notification-service")
public void on(OrderPlaced event) {
// Must be idempotent: the same event can be delivered more than once
System.out.println("Emailing confirmation for order " + event.orderId() + " to " + event.customerEmail());
}
}
Notice that each service declares its own copy of the OrderPlaced record. Services share a contract (the event's fields), not code, so they can be built and released independently. In production you would also use the outbox pattern so that saving the order and publishing the event cannot get out of step.
7. Designing for Failure
In a system of dozens of services, something is always failing somewhere: a pod restarting, a network blip, a slow database. Microservices must assume failure and contain it:
- Timeouts on every network call, shorter than the caller's own timeout.
- Retries with exponential backoff and jitter for temporary errors, only for idempotent operations. See Retry Pattern.
- Circuit breakers to stop hammering a failing dependency. See Circuit Breaker.
- Bulkheads that give each dependency its own limited pool of threads or connections, so one slow dependency cannot use them all. See Bulkhead Pattern.
- Fallbacks and graceful degradation: show cached recommendations or "stock unknown" rather than an error page.
- Idempotency: make operations safe to repeat, because retries and duplicate messages are normal.
- Health checks so the platform restarts unhealthy instances and stops routing traffic to them.
8. How to Split a System into Services
Choosing service boundaries is the most important design decision, and the hardest to change later. Good boundaries follow the business, not the technology:
- Use business capabilities: what the business does, such as catalogue, pricing, ordering, payments, shipping and customer support.
- Use Domain-Driven Design (DDD) bounded contexts: areas where a word has one clear meaning. A "product" in the catalogue (description, images) is different from a "product" in the warehouse (location, weight).
- Aim for high cohesion and low coupling: things that change together stay together; a typical feature should touch one service, not five.
- Align with teams: each service should be owned by one team. Systems tend to mirror the communication structure of the organisation that builds them (Conway's law).
- Start coarse: it is easier to split a service later than to merge two that were split too finely.
| Example service | Owns | Typical API or events |
|---|---|---|
| Catalogue | Products, descriptions, images, categories | GET /products/{id}, ProductUpdated |
| Pricing | Prices, discounts, promotions | POST /prices/quote |
| Order | Orders and their status | POST /orders, OrderPlaced, OrderCancelled |
| Payment | Payment attempts, refunds | PaymentSucceeded, PaymentFailed |
| Shipping | Shipments, carriers, tracking | ShipmentDispatched |
| Notification | Templates, delivery logs | Consumes events, sends email, SMS and push |
9. Deploying and Operating Microservices
- Containers: each service is packaged as a container image with everything it needs to run.
- Orchestration: Kubernetes or a managed container platform schedules containers, restarts failed ones, scales them and routes traffic.
- One pipeline per service: every service has its own automated build, test and deployment pipeline, so teams release independently.
- Safe releases: rolling, blue-green and canary deployments limit the impact of a bad release.
- Backward-compatible APIs: because services are released independently, changes must not break existing consumers. Add fields rather than renaming them, and version APIs when breaking changes are unavoidable.
- Infrastructure as code: environments are created and changed through version-controlled scripts.
10. Observability: Seeing Across Services
When one user request passes through six services, a problem can hide anywhere. Observability is not optional:
- Centralised, structured logs with a correlation ID (trace ID) included in every log line, so all logs for one request can be found together.
- Metrics for each service: request rate, error rate and latency percentiles, plus business metrics such as orders per minute.
- Distributed tracing (for example with OpenTelemetry) that shows the full path and timing of a request across services. Spring Boot supports this through Micrometer Tracing.
- Alerts based on user impact, using service-level objectives, rather than on every individual metric.
11. Advantages of Microservices
- Independent deployment: teams release on their own schedule, often many times a day.
- Independent scaling: scale only the services that are busy, such as search during a sale.
- Fault isolation: a failure in recommendations need not break checkout.
- Team autonomy: small teams own services end to end, which helps large organisations move quickly.
- Technology flexibility: choose the best tool for each job, and upgrade services one at a time.
- Easier to understand individual services: each codebase is small and focused.
12. Challenges of Microservices
- Distributed system complexity: network failures, latency, retries, timeouts and partial failures become everyday concerns.
- Data consistency: no cross-service transactions; sagas, outboxes and eventual consistency must be designed carefully.
- Operational overhead: many services to deploy, monitor, secure and upgrade, requiring strong automation and platform skills.
- Harder testing: end-to-end testing across services is slow and fragile; contract testing becomes important.
- Debugging: a single bug may involve logs and traces from several services.
- Performance overhead: network calls and serialisation are far slower than in-process calls.
- Higher cost: more infrastructure, more tooling and more specialised engineering time.
13. When to Use Microservices
Microservices pay off when:
- Several teams work on the same product and are slowed down by shared releases.
- Parts of the system have very different scaling, reliability or technology needs.
- The business domain is well understood, so service boundaries can be drawn with confidence.
- The organisation has mature automation: CI/CD, containers, monitoring and on-call practices.
For a small team, a new product or an unclear domain, a modular monolith is usually faster and cheaper. The detailed trade-offs are in Monolithic vs Microservices Architecture.
14. Common Mistakes to Avoid
- Building a distributed monolith: services that share a database or must be deployed together.
- Services that are too small: "nanoservices" that need many network calls for simple tasks.
- Long synchronous call chains: latency and failure probability multiply with every hop.
- No timeouts or circuit breakers: one slow service drags down the whole system.
- Sharing domain code libraries between services, which couples their release cycles.
- Skipping observability: without tracing and correlation IDs, incidents become guesswork.
- Starting with microservices before the team, the product or the automation is ready.
15. Best Practices
- Draw service boundaries around business capabilities and bounded contexts.
- Give every service its own database, and never share tables between services.
- Prefer asynchronous events for side effects, and keep synchronous call chains short.
- Use timeouts, retries with backoff, circuit breakers and bulkheads on every remote call.
- Make message consumers and APIs idempotent.
- Use an API gateway for clients, and keep services private behind it.
- Automate everything: builds, tests, deployments, infrastructure and security scanning.
- Invest in observability from day one: structured logs, metrics, tracing and correlation IDs.
- Use contract tests to keep APIs and events compatible between independently released services.
16. Summary
- Microservices architecture splits an application into small, independently deployable services, each owning one business capability and its data.
- A real system also needs an API gateway, service discovery, configuration, a message broker and observability.
- Services communicate synchronously for queries and asynchronously through events for side effects.
- Database per service brings independence but requires sagas, the outbox pattern and eventual consistency.
- Microservices enable team autonomy and independent scaling, at the cost of distributed-system complexity, so they suit larger teams with mature automation.
Microservices are a powerful tool for scaling organisations as much as software; use them when their benefits clearly outweigh their costs. Open the FAQ tab for answers to common questions about microservices architecture.
Frequently Asked Questions (FAQ)
Q1. What is microservices architecture in simple words?
It is a way of building an application as many small, independent services. Each service handles one business job, such as orders or payments, has its own database and can be developed, deployed and scaled separately. The services work together over the network.
Q2. What are the main characteristics of microservices?
Microservices are organised around business capabilities, independently deployable, own their data, communicate through APIs or events, are designed to tolerate failure, rely on automated delivery and are owned end to end by small teams.
Q3. How do microservices communicate with each other?
Synchronously, through REST or gRPC calls where the caller waits for a response, and asynchronously, through messages or events sent via a broker such as Kafka or RabbitMQ. Synchronous calls suit queries; events suit notifications and workflows.
Q4. What is the database-per-service pattern?
It means each microservice has its own private database or schema that no other service accesses directly. Other services get the data through the owner's API or events. This keeps services independent but makes cross-service transactions and queries harder.
Q5. How are transactions handled across microservices?
Instead of one distributed transaction, microservices use the saga pattern: each service performs a local transaction and triggers the next step, and if a later step fails, compensating actions undo the earlier steps. The transactional outbox pattern makes publishing the events reliable.
Q6. What is an API gateway in microservices?
An API gateway is the single entry point for clients. It routes requests to the right service and handles cross-cutting concerns such as authentication, rate limiting, TLS termination and sometimes combining responses from several services.
Q7. What is service discovery?
Service discovery is how services find the network locations of other services whose instances start, stop and move dynamically. A registry such as Consul or Eureka, or the built-in service mechanism of Kubernetes, keeps track of healthy instances.
Q8. What is a circuit breaker in microservices?
A circuit breaker wraps calls to a dependency. When too many calls fail, it opens and fails fast for a period instead of waiting on a broken service, then allows a few test calls to check recovery. It prevents cascading failures.
Q9. How small should a microservice be?
Small enough to be owned and understood by one team and focused on one business capability, but not so small that simple features need many network calls. Boundaries should follow the business domain, not a line-count target.
Q10. What are the advantages of microservices?
- Independent deployment and faster releases.
- Independent scaling of busy services.
- Fault isolation.
- Team autonomy in large organisations.
- Freedom to choose technology per service.
Q11. What are the disadvantages of microservices?
They add distributed-system complexity, make data consistency and transactions harder, increase operational and infrastructure cost, complicate testing and debugging, and introduce network latency between services.
Q12. Is Spring Boot good for microservices?
Yes. Spring Boot makes it quick to build standalone services, and the Spring ecosystem adds support for REST clients, messaging with Kafka and RabbitMQ, resilience with Resilience4j, configuration, observability with Micrometer and API gateways with Spring Cloud Gateway.
Q13. What is the difference between microservices and SOA?
Both build systems from services. SOA focuses on enterprise-wide reuse and integration, often with coarse-grained services, shared databases and an enterprise service bus. Microservices focus on independent deployment of fine-grained services that own their data and communicate through lightweight protocols.
Q14. What is a distributed monolith?
A distributed monolith is a set of services that are deployed separately but remain tightly coupled, for example by sharing a database or needing to be released together. It has the complexity of microservices without their independence, so it should be avoided.
Q15. How do you monitor microservices?
Use centralised structured logging with correlation IDs, metrics for request rate, errors and latency per service, distributed tracing to follow requests across services, health checks and alerts based on service-level objectives.
Q16. How do you deploy microservices?
Package each service as a container image, deploy it with its own CI/CD pipeline to an orchestration platform such as Kubernetes, and use rolling, blue-green or canary releases. Keep APIs backward compatible so services can be released independently.
Q17. How do you test microservices?
Combine unit tests, integration tests for each service with real dependencies such as databases (for example using Testcontainers), consumer-driven contract tests between services, and a small number of end-to-end tests for critical journeys.
Q18. Should every application use microservices?
No. Microservices suit large systems built by multiple teams with mature automation. Small teams and new products usually move faster with a modular monolith and can extract services later when a clear need appears.
Q19. What is eventual consistency in microservices?
Because each service has its own data and updates spread through events, different services may briefly show different information. Eventual consistency means they will agree once all events are processed, usually within milliseconds or seconds.
Q20. How should I explain microservices in an interview?
Define microservices as small, independently deployable services around business capabilities with their own data, describe the supporting components (gateway, discovery, broker, observability), compare synchronous and asynchronous communication, explain sagas for transactions, list resilience patterns, and discuss when the trade-offs are worth it.