Spring Boot
Monolithic vs Microservices
A monolithic application packages all functionality — UI, business logic, and data access — into a single deployable unit. A microservices architecture decomposes that unit into small, independently deployable services each owning a single business capability. Choosing between them involves trade-offs in complexity, scalability, team autonomy, and operational overhead.
Monolithic Architecture
In a monolith all modules — user management, orders, payments, notifications — are compiled and deployed together as one artifact (a WAR, JAR, or executable). The modules share a single process, a single database, and a single deployment pipeline. This is the natural starting point for most applications and remains the right choice for small teams and early-stage products.
Java
// ── Typical monolith package structure ───────────────────────────────
//
// ecommerce-app/
// ├── src/main/java/com/example/
// │ ├── user/
// │ │ ├── UserController.java
// │ │ ├── UserService.java
// │ │ └── UserRepository.java
// │ ├── order/
// │ │ ├── OrderController.java
// │ │ ├── OrderService.java
// │ │ └── OrderRepository.java
// │ ├── payment/
// │ │ ├── PaymentController.java
// │ │ ├── PaymentService.java
// │ │ └── PaymentRepository.java
// │ └── EcommerceApplication.java ← single entry point
// └── src/main/resources/
// └── application.yml ← one config for everything
//
// Deployed as: ecommerce-app.jar ← one artifact
// Database: one shared schema ← all tables in one DB
// ── Cross-module call inside a monolith: ─────────────────────────────
// Direct Java method call — no network, no serialisation overhead.
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserService userService; // injected by Spring
private final PaymentService paymentService;
public Order placeOrder(CreateOrderRequest req) {
User user = userService.findById(req.getUserId()); // in-process call
Order order = orderRepository.save(Order.from(req, user));
paymentService.charge(order); // in-process call
return order;
}
}
// ── Monolith advantages: ──────────────────────────────────────────────
// + Simple local development — one app to run
// + In-process calls: fast, no serialisation, no network failures
// + Single database: ACID transactions across modules trivially
// + Easy to debug: one log stream, one stack trace
// + Simple deployment: ship one artifact
// + Low operational overhead: no service mesh, no registry
// ── Monolith disadvantages (at scale): ───────────────────────────────
// - Any change requires redeploying the entire application
// - A bug in one module can crash the whole app
// - Scaling requires duplicating everything, even unrelated modules
// - Large codebase becomes difficult to navigate for big teams
// - Technology lock-in: every module must use the same stackMicroservices Architecture
Microservices split the monolith into independently deployable services, each responsible for one business domain. Each service has its own codebase, database, and deployment pipeline. Services communicate over the network via REST, gRPC, or messaging. This unlocks independent scaling and deployment but adds significant distributed-systems complexity.
Java
// ── Microservices project structure ──────────────────────────────────
//
// ecommerce/
// ├── user-service/ ← owns: registration, auth, profiles
// │ ├── src/...
// │ ├── Dockerfile
// │ └── application.yml (port: 8081, DB: users_db)
// │
// ├── order-service/ ← owns: cart, checkout, order history
// │ ├── src/...
// │ ├── Dockerfile
// │ └── application.yml (port: 8082, DB: orders_db)
// │
// ├── payment-service/ ← owns: billing, invoicing, refunds
// │ ├── src/...
// │ ├── Dockerfile
// │ └── application.yml (port: 8083, DB: payments_db)
// │
// └── api-gateway/ ← single entry point for clients
// └── application.yml (port: 8080)
//
// Each service deployed independently: docker push order-service:v2
// Each service has its OWN database — no shared schema.
// ── Cross-service call (network required): ───────────────────────────
@Service
@RequiredArgsConstructor
public class OrderService {
private final WebClient webClient; // HTTP client — network call
public Order placeOrder(CreateOrderRequest req) {
// Must call UserService over HTTP — UserService may be slow or down.
UserResponse user = webClient.get()
.uri("http://user-service/api/users/" + req.getUserId())
.retrieve()
.bodyToMono(UserResponse.class)
.block();
// No shared DB → cannot do a simple JOIN.
// Data consistency across services requires Saga / events.
return orderRepository.save(Order.from(req, user));
}
}
// ── Microservices advantages: ─────────────────────────────────────────
// + Deploy services independently — no big-bang releases
// + Scale only the services under load (e.g. scale OrderService ×10)
// + Team autonomy — each team owns their service end-to-end
// + Technology freedom — Python ML service + Java order service
// + Fault isolation — PaymentService crash does not kill UserService
// ── Microservices disadvantages: ─────────────────────────────────────
// - Network calls: latency, timeouts, retries, serialisation
// - No ACID across services — eventual consistency is hard
// - Distributed tracing needed to debug a single request
// - Operational overhead: service registry, gateway, health checks
// - More infrastructure: Docker, Kubernetes, CI/CD per serviceSide-by-Side Comparison
The table below summarises the key differences across the dimensions that matter most when choosing an architecture. Neither style is universally better — the right choice depends on team size, product maturity, and operational capability.
Java
// ┌──────────────────────┬─────────────────────┬──────────────────────┐
// │ Dimension │ Monolith │ Microservices │
// ├──────────────────────┼─────────────────────┼──────────────────────┤
// │ Deployment unit │ One artifact │ Many artifacts │
// │ Database │ Shared schema │ DB per service │
// │ Communication │ In-process call │ Network (REST/event) │
// │ Transactions │ ACID trivially │ Saga / eventual cons.│
// │ Scaling │ Scale everything │ Scale per service │
// │ Fault isolation │ One crash = all down│ Crash stays isolated │
// │ Team size │ Small–medium │ Medium–large │
// │ Operational overhead │ Low │ High │
// │ Technology choice │ One stack │ Per-service stack │
// │ Debugging │ Single log/trace │ Distributed tracing │
// │ Local dev setup │ Run one app │ Run many apps │
// │ Release cadence │ All-or-nothing │ Independent │
// └──────────────────────┴─────────────────────┴──────────────────────┘
// ── When to choose Monolith: ──────────────────────────────────────────
// • Small team (< 10 engineers)
// • Early-stage product — requirements change rapidly
// • No dedicated DevOps/platform team
// • Low traffic — scaling individual modules is not needed
// • Strong need for ACID transactions across domains
// ── When to choose Microservices: ────────────────────────────────────
// • Multiple teams that need to deploy independently
// • Clear, stable domain boundaries
// • Different modules have vastly different scaling requirements
// • Platform team exists to manage infrastructure
// • Regulatory/security isolation required between domains
// ── The Strangler Fig pattern — migrating from monolith: ─────────────
// 1. Run the new microservice alongside the monolith.
// 2. Route a specific endpoint/feature to the new service.
// 3. Gradually move more features over.
// 4. Retire the monolith module once fully migrated.
//
// Client → API Gateway → /api/users/** ──▶ UserService (new)
// → /api/** ──▶ Monolith (legacy)