Microservices Architecture & Design

Monolith vs Microservices

18 min Lesson 1 of 12

Monolith vs Microservices

Every software system starts somewhere. Most start as a monolith — a single deployable unit where every feature lives in the same process. Microservices are a deliberate reaction to the problems that monoliths develop at scale. This lesson examines both architectures honestly, because the single most important skill you need before adopting microservices is knowing when not to.

What a Monolith Actually Is

A monolith is not a synonym for "bad code." It is a deployment topology: one build artifact (.jar, .war, a container) that packages the entire application. Spring Boot applications are monoliths by default. When you run mvn spring-boot:run, your controllers, services, repositories, and domain logic all start in a single JVM.

Monoliths come in flavours. A modular monolith enforces clear package boundaries and dependency rules inside a single deployable. A big-ball-of-mud monolith has no enforced structure. The second kind is what people usually mean when they complain about monoliths — but the architecture itself is not the cause of that mess; lack of discipline is.

Key idea: A well-structured monolith is genuinely easier to operate than a poorly designed set of microservices. Microservices distribute complexity; they do not eliminate it.

The Problems That Make Teams Reach for Microservices

Monoliths develop predictable pain points as a codebase grows and teams expand. Understanding these pain points tells you whether microservices are actually the right cure.

  • Deployment coupling. A bug in the payments module forces you to redeploy the entire application — including the unrelated user-profile module. Risk and blast radius grow with the size of each release.
  • Scaling inflexibility. If only your image-processing feature is under load, you still have to scale the whole application. A single process means a single scaling unit.
  • Technology lock-in. Every part of the system must use the same language, runtime, and dependency versions. A team wanting to experiment with a different database or framework cannot do so without affecting everyone.
  • Team autonomy bottleneck. When twenty developers work in a single codebase, merge conflicts, test suite ownership, and shared release coordination become significant overhead.
  • Availability coupling. A memory leak or uncaught exception in one part of the process can crash the entire application.

What Microservices Trade for Those Solutions

Microservices decompose the application into independently deployable services, each owning its own data and communicating over the network. The Spring ecosystem gives you the building blocks: Spring Boot for each service, Spring Cloud for cross-cutting concerns, and a service registry such as Eureka or Consul for discovery.

A minimal two-service skeleton illustrates the shape. The order-service calls the inventory-service over HTTP:

// order-service — build.gradle (Spring Boot 3) dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' }
// InventoryClient.java (inside order-service) @FeignClient(name = "inventory-service", url = "${inventory.service.url}") public interface InventoryClient { @GetMapping("/api/v1/inventory/{sku}/available") boolean isAvailable(@PathVariable String sku, @RequestParam int quantity); }
// OrderService.java @Service @RequiredArgsConstructor public class OrderService { private final InventoryClient inventoryClient; private final OrderRepository orderRepository; public Order place(PlaceOrderRequest req) { if (!inventoryClient.isAvailable(req.sku(), req.quantity())) { throw new InsufficientStockException(req.sku()); } Order order = Order.create(req); return orderRepository.save(order); } }

What looks clean in a diagram carries real distributed-systems costs:

  • Network latency and partial failure. The HTTP call to inventory-service can time out, return a 503, or never arrive. In a monolith, calling a service is a method call — effectively free and always synchronous.
  • Distributed tracing. A single user request now spans multiple processes. Without a correlation ID propagated in headers and a tool like Zipkin or OpenTelemetry, debugging is guesswork.
  • Data consistency. You cannot wrap the inventory check and the order insert in a single database transaction. You need eventual consistency patterns (sagas, outbox) that simply do not exist in a monolith.
  • Operational surface area. Ten services means ten CI/CD pipelines, ten container images, ten health endpoints to monitor, ten sets of secrets to rotate, and ten places for configuration drift.
Distributed systems failure modes are qualitatively different. A monolith either works or it does not. A microservices system can be partially degraded in ways that are harder to detect and reason about. Build in circuit breakers (Resilience4j), timeouts, and retries from day one — not as an afterthought.

The Security Dimension

Security changes fundamentally when you decompose a monolith. In a monolith, Spring Security enforces authentication once at the edge, and every subsequent method call is implicitly trusted. In a microservices system, every service-to-service call crosses a network boundary and is a potential attack surface.

The standard answer is JWT propagation: the API gateway validates the incoming token, then forwards it (or a service-account token) to downstream services. Each service independently verifies the token signature.

// SecurityConfig.java — each downstream service @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(AbstractHttpConfigurer::disable) // stateless service .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) // verify JWT signature .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .build(); } }

You now have to manage key rotation, token expiry, and service-to-service trust across every service — complexity that is entirely absent in a monolith where one SecurityFilterChain covers everything.

When Microservices Are Worth It — and When They Are Not

Use this as a practical checklist before committing to the split:

  • Team size and autonomy matter more than code size. If two teams need to deploy the same module on different schedules without coordinating, that is a genuine microservices use case. If one team owns everything, a modular monolith gives most of the benefit at a fraction of the cost.
  • Start with a monolith, extract later. You almost never know the right service boundaries upfront. Prematurely splitting creates wrong boundaries that are expensive to undo once the services have separate databases and separate teams.
  • Independent scaling requirements are a strong signal. If one part of your system needs 10× the compute of another, and those parts can be cleanly separated, that is a real reason to split.
  • Organisational maturity matters. Microservices require DevOps capability: automated deployment, container orchestration, centralised logging, distributed tracing, and service mesh or mTLS. A team without those practices will drown in operational overhead.
The "two-pizza team" heuristic: One microservice should be small enough to be owned end-to-end by a team that can be fed by two pizzas (~5–8 people). If you are a solo developer or a team of three, a modular monolith almost certainly serves you better right now.

Practical Starting Point

If you are building a new system, start here: structure your Spring Boot application as a modular monolith with clear package boundaries (com.example.orders, com.example.inventory). Define explicit interfaces between modules. Avoid cross-module direct field access. When a module's deployment or scaling requirements genuinely diverge — and you can observe that divergence in production data — extract it into a separate service at that point.

This approach gives you a clean extraction path because you have already thought about boundaries, without paying the operational tax of microservices before you need to.

Summary

Monoliths and microservices are deployment topologies, not quality levels. Monoliths are simpler to build, test, debug, and secure. Microservices solve real problems — independent deployment, fine-grained scaling, and team autonomy — but introduce distributed-systems complexity, network failure modes, and significant operational overhead. The right architecture is the one that fits your team size, deployment cadence, and operational maturity. For most teams at most stages, that means starting with a well-structured monolith and migrating selectively, not rewriting everything as services from day one.