Microservices6 min read·By Liyabona Saki··

Spring Boot Microservices Architecture Explained Step by Step

A complete, beginner-friendly walkthrough of microservices architecture using Spring Boot — services, gateway, discovery, config and observability.

▶ Watch this tutorial on MasterLabLearn on YouTube — and subscribe for more.

Why I stopped recommending microservices for small teams

The first production microservices system I shipped had eleven services and three engineers. It did not go well. We spent more time chasing distributed bugs across service boundaries than we ever spent writing features. Two years later I rebuilt the same product as a modular monolith and we shipped twice as fast.

That experience changed how I teach this topic. Spring Boot makes it trivially easy to spin up another service — spring init, a new repo, a new pipeline — and that low friction is precisely the trap. The architecture diagram looks impressive in slides. The on-call rotation does not.

This article is a practical walkthrough of a Spring Boot microservices setup that I would actually defend in a code review today: small, observable, and built so you can collapse it back into a monolith without rewriting business logic.

The smallest defensible setup

If you are going to commit to microservices, the minimum useful topology is:

text
client -> gateway -> [orders] -> [payments]
                  \-> [catalog]
                          |
                       config server + discovery

Three services, one gateway, one platform layer. Anything smaller is a distributed monolith. Anything larger before you have observability is malpractice.

The gateway (Spring Cloud Gateway) is non-negotiable. It is the only thing your client should know about. It handles routing, auth verification, and rate limiting. Without it, every service ends up reimplementing JWT parsing and CORS — and one of them will get it wrong.

What goes in a service

A service owns one *business capability* end to end: its data, its HTTP surface, its background jobs. The fastest way to design badly is to split by technical layer ("the auth service", "the database service"). Split by business noun: orders, payments, catalog, shipping.

A useful test: if changing one feature requires a coordinated deploy across two services, the split is wrong. Merge them.

Inter-service communication, the boring answer

You will hear strong opinions about REST vs gRPC vs messaging. The boring answer is:

  • Synchronous reads between services: REST with OpenFeign. Simple, debuggable, easy to mock in tests.
  • Anything that triggers a side effect in another service: events on Kafka, never a synchronous call.

The reason is failure mode. If service A calls service B synchronously to "create an invoice when an order is placed", a slow B will cause A's HTTP threads to back up, and the whole system tips over. If A publishes order.placed and B subscribes, A keeps responding even when B is down.

I have shipped exactly this anti-pattern in production. The post-mortem is what convinced me to switch.

Config and discovery

spring-cloud-config-server reads YAML from a Git repo and serves it to your services on startup. The win is not "centralized config" — it is *auditability*. Every config change is a commit. When something breaks at 2am, git log tells you who changed what.

For discovery, Eureka is fine for small fleets. Once you are on Kubernetes, throw Eureka away and use the cluster DNS — it is one less thing to operate.

Observability is the actual hard part

The single most useful thing you can do is propagate a trace ID through every service and log it on every line. Spring Cloud Sleuth (or its Micrometer Tracing successor) does this for you. Wire it up on day one, before you write any business logic.

Without distributed tracing, a "slow checkout" bug means staring at five log files trying to correlate timestamps. With it, you click one trace and see the whole request span every service it touched.

When NOT to do this

Use a modular monolith if:

  • Your team is under 8 engineers
  • You do not yet have a paying customer
  • You cannot articulate which service will need to scale independently

A modular monolith with clean package boundaries gives you 90% of the architectural benefits and none of the operational pain. You can extract a true service later, once you actually know which seam will hold up.

The microservices pattern is a tool for a specific scaling problem. It is not a maturity signal, and it is not free. Reach for it when the cost of staying with a monolith is provably higher than the cost of distribution — and not a day sooner.

The Spring-specific gotchas worth knowing

A few things that bit me and would have saved hours if I had known:

  • The default Tomcat connection pool on a service that calls another service should be tuned together with the *callee's* thread pool. Mismatched pools is the #1 cause of mysterious 504s.
  • Do not share entity classes across services through a "common" library. Duplicate the DTOs. Coupling through a shared JAR is worse than the duplication you are avoiding.
  • Spring Boot 3 + native compilation is wonderful for cold-start sensitive services (Lambda, scale-to-zero) but rough for anything that uses dynamic proxies. Profile before committing.

If you take one thing from this article, take this: build a modular monolith first, add a trace ID and structured logs from day one, and only extract a service when you can name the specific scaling or team-autonomy problem it solves.

Go deeper

Further reading

Source Code

Get the full project on GitHub

View repo →
#Spring Boot#Microservices#Architecture#Spring Cloud

Stay in the Loop

Get the next tutorial in your inbox

Continue reading in Microservices

How to Build a Spring Cloud Config Server

Step-by-step guide to building a centralized configuration server with Spring Cloud Config, Git-backed properties and dynamic refresh.

Related tutorials