00:00

Microservices Introduction

Welcome to our Microservices tutorial! Whether you are a beginner or an experienced developer, this guide will help you understand microservices architecture step by step, with simple explanations, diagrams and working Java examples.

You will learn what microservices are, why companies use them, the building blocks of a microservices system, and how to build, connect and run your first services with Spring Boot and Docker. Along the way, you will find links to detailed guides on each topic.

1. What Is Microservices Architecture?

In simple terms, microservices architecture is a way of building software in which an application is made up of a collection of small, independent and loosely coupled services. Each service handles one business capability, runs in its own process, and talks to other services over the network.

Think of it like building with Lego blocks. Instead of carving a complex structure from one large piece of wood (the monolith), you build it by connecting many small, specialized and replaceable blocks. If one block needs to change, you swap that block without rebuilding the whole model.

Monolith vs microservices
      Monolith                         Microservices

+----------------------+      +--------+  +--------+  +--------+
|  Users               |      | Users  |  |Products|  | Orders |
|  Products            |      +---+----+  +---+----+  +---+----+
|  Orders              |          |           |           |
|  Payments            |       [ DB ]      [ DB ]      [ DB ]
+----------+-----------+
           |                  Each service: own code, own data,
     [ One Database ]         own deployment

2. Why Microservices? From Monolith to Services

Most applications start as a monolith: one codebase, one build and one database. That is simple and works well at first. As the application and the team grow, problems appear:

  • A small change requires rebuilding and redeploying the whole application.
  • Many teams work in the same codebase and get in each other’s way.
  • To handle more traffic on one feature, you must scale the entire application.
  • One bug, such as a memory leak, can crash everything.

Microservices address these problems by splitting the application along business boundaries, so each part can be built, deployed and scaled on its own. Companies such as Netflix, Amazon and Uber popularized this approach to let hundreds of teams work and release independently. For a detailed comparison, see Monolithic vs Microservices Architecture.

3. Key Characteristics of Microservices

CharacteristicWhat it means
Single responsibilityEach service focuses on doing one business thing well, such as payments, and owns the data and logic for it
Independent developmentTeams work on different services at the same time without stepping on each other’s toes
Independent deploymentYou can update one service without redeploying the whole application
Technology freedomEach service can use the programming language that suits it (polyglot programming) and the database that suits it (polyglot persistence)
Decentralized dataEach service has its own database, which avoids the tight coupling a shared database creates
Communication through APIsServices talk over the network with lightweight protocols such as HTTP/REST, or through message brokers such as RabbitMQ and Kafka
Built for failureServices can fail, so the system is designed to cope, using patterns such as circuit breakers and retries
AutomationMany services are only manageable with automated builds, tests, deployments and monitoring

4. Building Blocks of a Microservices System

A real microservices system needs more than the services themselves. These supporting pieces appear in almost every architecture:

Building blockPurposeCommon toolsLearn more
API gatewaySingle entry point for clients; routing, security, rate limitingSpring Cloud Gateway, Kong, AWS API GatewayAPI Gateway
Service discoveryLets services find each other by name instead of fixed addressesEureka, Consul, Kubernetes DNSService Discovery
Database per serviceEach service owns and controls its dataPostgreSQL, MongoDB, RedisDatabase per Service
Message brokerAsynchronous communication through eventsApache Kafka, RabbitMQEvent-Driven Architecture
ResilienceTimeouts, retries and circuit breakers for unreliable networksResilience4jCircuit Breaker
ObservabilityLogs, metrics and traces across all servicesMicrometer, Prometheus, Grafana, ZipkinSection 14
Containers and orchestrationPackage and run many services consistentlyDocker, KubernetesSection 9
Central configurationManage settings for all services in one placeSpring Cloud Config, Kubernetes ConfigMaps—

5. Example: An Online Shop as Microservices

Let us design a simple online shop. Each service owns one business capability and its own data:

ServiceResponsibilityOwns data about
user-serviceRegistration, login and profilesUsers and addresses
product-serviceProduct catalog and pricesProducts
order-serviceCreating and tracking ordersOrders and order lines
payment-serviceCharging customers and refundsPayments
notification-serviceEmails and text messagesSent notifications
Online shop architecture
           Web app / Mobile app
                   |
            [ API Gateway ]
                   |
   +-------+-------+-------+--------+
   |       |       |       |        |
 User   Product  Order  Payment  Notification
   |       |       |       |        |
 [DB]    [DB]    [DB]    [DB]     [DB]

  Order --(REST)--> Product        (check price and stock)
  Order --(event)--> [ Kafka ] --> Payment, Notification

The order service asks the product service for prices synchronously, because it needs the answer right away. After an order is placed, it publishes an event, and the payment and notification services react asynchronously. The next sections build this step by step.

6. Building Your First Microservice with Spring Boot

Spring Boot is the most popular way to build microservices in Java. A microservice is simply a Spring Boot application with a focused job. Let us build the product service.

6.1 Dependencies

Create a project at start.spring.io with these dependencies, or add them to pom.xml:

pom.xml
<dependencies>
    <!-- REST endpoints -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Database access -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- Health checks and metrics -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- In-memory database for learning -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

6.2 Configuration

application.yml
server:
  port: 8081

spring:
  application:
    name: product-service
  datasource:
    url: jdbc:h2:mem:products

management:
  endpoints:
    web:
      exposure:
        include: health,info

Each service runs on its own port locally and has its own name, which is used later for service discovery, logs and tracing.

6.3 The entity and repository

Product.java
package com.example.product;

import java.math.BigDecimal;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    private int stock;

    protected Product() {
        // required by JPA
    }

    public Product(String name, BigDecimal price, int stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
    public int getStock() { return stock; }
}
ProductRepository.java
package com.example.product;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

6.4 The REST controller

ProductController.java
package com.example.product;

import java.math.BigDecimal;
import java.net.URI;
import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductRepository repository;

    public ProductController(ProductRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public List<Product> findAll() {
        return repository.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> findById(@PathVariable Long id) {
        return repository.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Product> create(@RequestBody NewProduct request) {
        Product saved = repository.save(
                new Product(request.name(), request.price(), request.stock()));
        return ResponseEntity
                .created(URI.create("/products/" + saved.getId()))
                .body(saved);
    }

    public record NewProduct(String name, BigDecimal price, int stock) {
    }
}

6.5 Try it

Terminal
# Start the service
./mvnw spring-boot:run

# Create a product
curl -X POST http://localhost:8081/products \
     -H "Content-Type: application/json" \
     -d '{"name": "Wireless Mouse", "price": 24.99, "stock": 100}'

# Read it back
curl http://localhost:8081/products/1

# Check the health endpoint
curl http://localhost:8081/actuator/health

That is a complete, working microservice: it owns its data, exposes a small REST API and reports its health.

7. Calling Another Microservice

The order service needs product prices. It calls the product service over HTTP using Spring’s RestClient. The address comes from configuration, so it can differ between your laptop, Docker and production:

application.yml (order-service)
server:
  port: 8082

spring:
  application:
    name: order-service

services:
  product:
    url: http://localhost:8081
ProductClient.java
package com.example.order.client;

import java.math.BigDecimal;
import java.time.Duration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class ProductClient {

    private final RestClient restClient;

    public ProductClient(RestClient.Builder builder,
                         @Value("${services.product.url}") String productServiceUrl) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(Duration.ofSeconds(2));
        factory.setReadTimeout(Duration.ofSeconds(3));

        this.restClient = builder
                .baseUrl(productServiceUrl)
                .requestFactory(factory)
                .build();
    }

    public ProductInfo getProduct(Long productId) {
        return restClient.get()
                .uri("/products/{id}", productId)
                .retrieve()
                .body(ProductInfo.class);
    }

    // The order service keeps its own view of a product: only the fields it needs
    public record ProductInfo(Long id, String name, BigDecimal price, int stock) {
    }
}

Notice two habits worth building from day one: always set timeouts on remote calls, and give each service its own small model of the data it receives, instead of sharing classes between services. Learn more in Synchronous Communication in Microservices.

8. Communicating with Events

Not every interaction needs an immediate answer. After an order is placed, the order service can simply announce it with an event. Other services react in their own time, and the order service does not even need to know who they are:

NotificationListener.java (notification-service)
@Component
public class NotificationListener {

    private final EmailService emailService;

    public NotificationListener(EmailService emailService) {
        this.emailService = emailService;
    }

    @KafkaListener(topics = "order-placed", groupId = "notification-service")
    public void onOrderPlaced(OrderPlacedEvent event) {
        emailService.sendOrderConfirmation(event.customerEmail(), event.orderId());
    }
}

If the notification service is down for a few minutes, the events wait in Kafka and are processed when it comes back, so a problem in one service does not stop customers from ordering. See Communication in Microservices and Asynchronous Communication for the full picture.

9. Running Microservices with Docker

Microservices are usually packaged as containers, so every service runs the same way on every machine. A simple Dockerfile for the product service, after building the JAR with ./mvnw package:

Dockerfile
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/product-service-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]

Docker Compose starts several services together. Inside Compose, services reach each other by their service name:

docker-compose.yml
services:
  product-service:
    build: ./product-service
    ports:
      - "8081:8081"

  order-service:
    build: ./order-service
    ports:
      - "8082:8082"
    environment:
      # Overrides services.product.url from application.yml
      SERVICES_PRODUCT_URL: http://product-service:8081
    depends_on:
      - product-service
Terminal
docker compose up --build

In production, an orchestrator such as Kubernetes takes over: it runs several copies of each service, restarts failed ones, balances traffic and scales services up and down automatically.

10. Advantages of Microservices

  • Agility and faster development: small, autonomous teams develop, test and deploy their services quickly and independently.
  • Targeted scalability: scale only the services that need it, such as the payment service during a sale, which is more cost-effective.
  • Resilience: the failure of one service does not have to crash the whole system; the rest can keep working, perhaps with reduced features.
  • Technology freedom: teams can choose the best tools for their specific job.
  • Easier to understand: a new developer only needs to learn one focused service to start contributing.
  • Faster, safer releases: small deployments are less risky and easier to roll back.

11. Challenges and Disadvantages

  • Complexity: managing many distributed services is much harder than one application. This is often called distributed system complexity.
  • Network latency and failures: network calls are slower than method calls and can fail, so the system must be designed to handle this.
  • Data consistency: keeping data consistent across services with separate databases is hard. It is often solved with the Saga pattern instead of traditional ACID transactions.
  • Operational overhead: you need solid DevOps practices: containers (Docker), orchestration (Kubernetes), central logging, monitoring and tracing.
  • Debugging: following a request through many services requires distributed tracing tools.
  • Testing: you need contract tests and some end-to-end tests in addition to unit tests.

12. Essential Microservices Patterns

PatternProblem it solves
API GatewayOne entry point for clients instead of calling every service directly
Service Discovery and Service RegistryFinding services whose addresses change
Database per ServiceKeeping services independent at the data level
AggregatorCombining data from several services into one response
Circuit BreakerStopping calls to a failing service
Retry and TimeoutHandling short failures and slow responses
BulkheadStopping one slow dependency from using up all resources
Rate LimitingProtecting services from too much traffic
SagaKeeping data consistent across services without one transaction
SidecarAdding features such as logging or proxies next to a service
Domain-Driven DesignFinding good boundaries between services

13. When to Use Microservices, and When Not To

Microservices are a good fit when…A monolith is usually better when…
The system is large and complex, with clear business areasThe application is small or simple
Many teams need to work and release independentlyOne small team builds the whole product
Different parts need very different scalingTraffic is moderate and even
You have DevOps skills: automation, containers, monitoringInfrastructure and operations experience is limited
The business domain is well understoodThe product is new and still changing direction quickly

Microservices are not a silver bullet. For a startup or a simple application, a well-structured monolith is often the more practical choice, and it can be split later when there is a clear need. See Use Cases of Microservices for real-world examples.

14. Best Practices for Beginners

  1. Design around business capabilities, not technical layers.
  2. Give each service its own database; never share tables between services.
  3. Keep services small enough for one team, but not so small that every feature touches many services.
  4. Set timeouts on every remote call, and add retries and circuit breakers where needed.
  5. Prefer events for work that does not need an immediate answer.
  6. Automate everything: builds, tests, deployments and infrastructure.
  7. Add observability from day one: health checks, metrics, central logs and distributed tracing with correlation IDs.
  8. Version your APIs and keep changes backward compatible.
  9. Secure every service, including traffic between services.
  10. Start simple: begin with a few services, or a modular monolith, and split further only when needed.

15. Your Learning Roadmap

Microservices is a large topic. A good order for learning it on this site:

  1. Monolithic vs Microservices Architecture: understand the trade-offs.
  2. Domain-Driven Design: learn how to find service boundaries.
  3. Communication in Microservices: synchronous and asynchronous styles.
  4. API Gateway and Service Discovery: connect services together.
  5. Database per Service and Saga: manage data.
  6. Circuit Breaker, Retry and Bulkhead: build resilience.
  7. Event-Driven Architecture: decouple services with events.

16. Summary

Microservices architecture builds an application from small, independent services, each owning one business capability and its own data, and communicating through APIs and events. It brings agility, targeted scaling, resilience and team independence, but also distributed complexity, harder data consistency and higher operational demands. With Spring Boot you can build a focused service in a few classes, connect services with RestClient or Kafka, and run them together with Docker. Use microservices for large, complex systems with several teams, and start with a well-structured monolith when the application or team is small. Continue with Monolithic vs Microservices Architecture to take the next step.