Microservices with Spring: When and Why

Introduction

A microservice architecture splits one application into several small applications, each with its own code base, its own database and its own deployments, which talk to each other over the network. Spring Boot made such services easy to write, and Spring Cloud adds what a group of them needs to work together. This tutorial starts the module on microservices with the question that should come first: when is the split worth it, and what does it cost? We will compare a monolith with microservices, build two small services that call each other, see with a real run what happens when one of them is down, and map the problems that appear to the Spring Cloud projects that the next tutorials cover. The code runs on Spring Boot 4.1.2 with Java 25.

Monolith and microservices

A monolith is one deployable application. Its modules - say catalog, orders and payments of a bookshop - call each other with ordinary method calls, share one database and one transaction, and are built, tested and released together. In a microservice architecture each of those modules is an application of its own: a separate process with a separate database, released on its own schedule, and reachable by the others only through its API - HTTP, gRPC or messages. A monolith - one process with catalog, orders and payments modules calling each other by method calls over one database - next to microservices - three processes, each with its own database, calling each other over the network The picture shows the two things that change. Calls between modules become network calls, and the one database becomes several databases that no single transaction covers. Almost every advantage and every cost of microservices follows from these two changes.

What you gain and what you pay

What microservices give -
  • Independent deployment: a team releases its service without waiting for, or retesting, the rest of the system. This is the main benefit, and it is an organisational one - it matters when several teams work on one product.
  • Independent scaling: the service under load gets ten instances while the others keep two.
  • Fault isolation: a memory leak in the recommendation service does not take the checkout down - if the callers are written for it.
  • Freedom of technology: each service picks the database and the framework version that fit it.
What they cost -
  • The network: a call can be slow, fail, or succeed without the answer arriving. A method call inside a monolith does none of that.
  • No shared transaction: an order that touches the order database and the payment database cannot simply roll back.
  • Operations: ten services mean ten pipelines, ten sets of configuration, logs in ten places, and a request that crosses four of them when something goes wrong.
  • Changing an API needs coordination between teams, where a monolith needs a refactoring in the IDE.

Two services that call each other

To make the first cost concrete, here are two Spring Boot applications. The catalog service knows the books and their prices -
catalog-service: src/main/java/.../catalog/CatalogApp.java
//CatalogApp.java
package com.javabrahman.cloud.intro.catalog;

import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.RestController;

@SpringBootApplication
@RestController
public class CatalogApp {

  record Book(String isbn, String title, int price) { }

  private final Map<String, Book> books = Map.of(
      "978-1", new Book("978-1", "Effective Java", 450),
      "978-2", new Book("978-2", "Spring in Action", 399));

  @GetMapping("/books/{isbn}")
  ResponseEntity<Book> book(@PathVariable String isbn) {
    return ResponseEntity.ofNullable(books.get(isbn));
  }

  public static void main(String[] args) {
    SpringApplication.run(CatalogApp.class, args);
  }
}
catalog-service: src/main/resources/application.properties
spring.application.name=catalog-service
server.port=8081
The order service takes orders. To price an order it needs the book, which belongs to the catalog service, so it asks for it over HTTP with a RestClientClick to read tutorial: Calling REST Services with RestClient and HTTP Interfaces -
order-service: src/main/java/.../order/OrderApp.java
//OrderApp.java
package com.javabrahman.cloud.intro.order;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@SpringBootApplication
@RestController
public class OrderApp {

  record Book(String isbn, String title, int price) { }
  record OrderRequest(String isbn, int quantity) { }
  record Order(String title, int quantity, int total) { }

  private final RestClient catalog;

  OrderApp(RestClient.Builder builder, @Value("${catalog.url}") String catalogUrl) {
    this.catalog = builder.baseUrl(catalogUrl).build();
  }

  @PostMapping("/orders")
  Order order(@RequestBody OrderRequest request) {
    // inside a monolith this line would be a method call: catalog.findBook(isbn)
    Book book = catalog.get().uri("/books/{isbn}", request.isbn()).retrieve().body(Book.class);
    return new Order(book.title(), request.quantity(), book.price() * request.quantity());
  }

  public static void main(String[] args) {
    SpringApplication.run(OrderApp.class, args);
  }
}
order-service: src/main/resources/application.properties
spring.application.name=order-service
server.port=8080
catalog.url=http://localhost:8081
With both applications running, an order works. Then the catalog service is stopped, and the same order is sent again -  OUTPUT of an order with the catalog service up, and then down
> curl -i -X POST http://localhost:8080/orders -H "Content-Type: application/json" -d '{"isbn":"978-1","quantity":2}'
HTTP/1.1 200
Content-Length: 51
Content-Type: application/json

{"title":"Effective Java","quantity":2,"total":900}

# (the catalog service is stopped now)

> curl -i -X POST http://localhost:8080/orders -H "Content-Type: application/json" -d '{"isbn":"978-1","quantity":2}'
HTTP/1.1 500
Content-Type: application/json

{"timestamp":"2026-11-10T05:18:48.957Z","status":500,"error":"Internal Server Error","path":"/orders"}
The second order failed with 500 Internal Server Error, although nothing is wrong with the order service or with the request. The log of the order service shows the cause -  From the log of the order service
2026-11-10T10:48:48.888+05:30 ERROR 31092 --- [order-service] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8081/books/978-1": null] with root cause
This is the new failure mode. In a monolith, the catalog module cannot be "down" while the order module runs. Between services it can, and it will - during a deployment, a crash, or a network problem - and the address http://localhost:8081 written into a properties file will be wrong the moment the catalog service moves or gets a second instance. One line of code turned into a list of questions -
  • Where is the catalog service right now, and which of its instances should be called?
  • What should the order service do when the call fails or takes ten seconds?
  • Where do twenty services get their configuration from, and how is it changed without rebuilding them?
  • How do clients outside reach the services, and where are they authenticated?
  • When one request passes through five services, how do you find out where it failed?
  • How does an order that needs a payment and a stock reservation stay consistent without a shared transaction?

What Spring Cloud adds

Spring Cloud is a family of projects built on Spring Boot, released together as a release train named by year - this module uses Spring Cloud 2025.1, the train for Spring Boot 4. Each project answers one of the questions above, and the next tutorials take them one by one - The building blocks of a microservice system - clients call an API gateway, which routes to services registered in a service registry; the services read from a config server, call each other through circuit breakers, exchange messages through a broker, and send traces to a tracing backend Two of these - retries and concurrency limits - also exist in Spring Framework 7 itself, as the tutorial on @Retryable and @ConcurrencyLimitClick to read tutorial: Resilience in Spring 7: @Retryable and @ConcurrencyLimit showed; circuit breakers are what a library such as Resilience4j adds.

When to choose microservices

The costs are paid from the first day; the benefits arrive only if the situation asks for them. Microservices fit when -
  • Several teams work on one system and block each other's releases. Independent deployment is the benefit that justifies the rest.
  • Parts of the system have very different load or availability needs - a search that needs fifty instances next to an admin screen that needs one.
  • The boundaries are known: the business areas have been stable for a while, and it is clear which data belongs to whom.
  • The organisation can operate them: automated deployment, central logging, monitoring and tracing are in place or will be built first.
They do not fit when -
  • One small team builds the whole product. It gets every cost and none of the organisational benefit.
  • The domain is new. Boundaries drawn too early are usually wrong, and moving a boundary between services is far harder than moving a class between packages.
  • The goal is cleaner code. A badly structured monolith split into services becomes a badly structured distributed system - a "distributed monolith" where every change still touches several services, which must now be deployed together.
  • Most operations need one transaction across several modules, and nobody has decided what should happen when half of such an operation fails.
A decision guide - start with a modular monolith when the team is small, the domain is new or boundaries are unclear; extract a service when a module needs its own release cycle, its own scaling or its own team

The modular monolith first

The usual advice in 2026 is to start with a modular monolith: one deployable application whose modules have clear boundaries - separate packages, no access to each other's tables, communication through a small API or through events. It keeps method calls and one transaction, and it keeps the option open: a module with a clean boundary can be extracted into a service when a reason from the first list appears, and only that module. Spring supports this style with Spring Modulith, which verifies the module boundaries in a test.
Whichever way a system starts, a service should own its data. Two services that read and write the same tables are one application in two processes: they cannot be released independently, which was the point.

Summary

In this tutorial we looked at what a microservice architecture changes: calls between modules become network calls, and one database becomes several. That buys independent deployment, scaling and fault isolation, and costs a network that can fail, the loss of shared transactions, and much more to operate. Two small Spring Boot services showed the first cost - an order failed with a 500 because the catalog service was down - and the questions that follow from it map to the Spring Cloud projects of this module: configuration, discovery, gateway, resilience, tracing, communication and sagas. Microservices pay off for several teams with stable boundaries and working operations; a small team or a new domain is better served by a modular monolith. The next tutorial begins with the first building block: centralized configuration with Spring Cloud ConfigClick to read tutorial: Centralized Configuration with Spring Cloud Config.