00:00

Microservices Architecture in System Design

Last updated:

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

CharacteristicMeaning
Organised around business capabilitiesServices map to business areas (catalogue, checkout, shipping), not technical layers (UI, logic, database)
Independently deployableA team can release its service many times a day without coordinating with other teams
Decentralised dataEach service has its own database or schema; others access the data only through the service's API or events
Smart endpoints, simple pipesBusiness logic lives inside services; the network and message brokers just move messages
Technology freedomEach service can choose the language, framework and database that fit its job, within organisation guidelines
Designed for failureServices expect their dependencies to be slow or unavailable and handle it gracefully
Automated deliveryBuilds, 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:

Building blocks of a microservices system Web and mobile clients call an API gateway. The gateway routes requests to an Order service, Inventory service, Payment service and Notification service, each with its own database. Services exchange events through a message broker. Along the bottom are platform services: service discovery and configuration, and observability with logs, metrics and traces. Web & mobile API Gateway Order Serviceowns: orders database Inventory Serviceowns: stock database Payment Serviceowns: payments database Notification Serviceowns: templates, email provider Message Broker Kafka · RabbitMQ events between services Service discovery & configuration Observability: logs · metrics · traces
Figure 1: A typical microservices system: an API gateway in front, independent services that each own their data, a message broker for events, and shared platform services for discovery, configuration and observability.
  • 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:

Synchronous and asynchronous communication between microservices Left: synchronous communication. The order service calls the inventory service over HTTP, which calls the payment service, and the client waits for the whole chain; if payment is down, the order fails. Right: asynchronous communication. The order service publishes an OrderPlaced event to a broker, and the inventory, email and analytics services consume it independently. SYNCHRONOUS (REQUEST / RESPONSE) Order Inventory Payment (down) HTTP HTTP client waits for the whole chain one slow service slows everyone ASYNCHRONOUS (EVENTS) Order Brokertopic: order-events OrderPlaced Inventory Email Analytics each consumer works independently a slow consumer does not block orders
Figure 2: Synchronous calls are simple but chain latency and failures together. Asynchronous events decouple services, so each consumer works at its own pace.
AspectSynchronous (REST, gRPC)Asynchronous (events, messages)
How it worksCaller sends a request and waits for the replyProducer publishes a message; consumers process it later
CouplingCaller needs the callee to be up right nowProducer and consumers are decoupled in time
LatencyAdds up along the call chainProducer returns immediately; work completes later
ConsistencyImmediate answerEventual: consumers catch up after a delay
ComplexitySimple to understand and debugNeeds a broker, retries, idempotent consumers and monitoring of lag
Best forQueries 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:

HttpClientConfig.java
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:

InventoryClient.java
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);
    }
}
application.yml
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:

OrderEventPublisher.java (order-service)
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
    }
}
OrderEventsListener.java (notification-service)
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:

Circuit breaker states Three states. Closed: calls flow normally. When failures exceed a threshold, the breaker moves to Open, where calls fail fast. After a wait time it moves to Half-open and allows a few test calls. If they succeed it returns to Closed; if they fail it goes back to Open. CLOSED calls pass through OPEN fail fast, use fallback HALF-OPEN a few test calls allowed failure rate above threshold wait time passes tests succeed tests fail Circuit breaker around calls to a dependency
Figure 3: A circuit breaker stops calling a failing service for a while, then carefully tests whether it has recovered. This prevents one broken dependency from exhausting every caller.
  • 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 serviceOwnsTypical API or events
CatalogueProducts, descriptions, images, categoriesGET /products/{id}, ProductUpdated
PricingPrices, discounts, promotionsPOST /prices/quote
OrderOrders and their statusPOST /orders, OrderPlaced, OrderCancelled
PaymentPayment attempts, refundsPaymentSucceeded, PaymentFailed
ShippingShipments, carriers, trackingShipmentDispatched
NotificationTemplates, delivery logsConsumes 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.