spring-boot-architecture
Spring Boot architecture & patterns
Request flow (why the layers exist)
DispatcherServlet is the front controller — it receives every HTTP request, HandlerMapping routes it to a controller method, the controller delegates to a service, the service applies business rules and calls repositories, and the response serializes back out (JSON for a REST API). Each layer exists to isolate one concern: controllers translate HTTP ↔ domain calls, services own business rules and transaction boundaries, repositories own data access.
Service Layer pattern
Services are the application's boundary — they centralize business rules so controllers stay thin (routing, status codes, request/response shaping) and repositories stay thin (queries only). A multi-repository-call operation's transaction boundary belongs in the service, via @Transactional, not scattered across repository calls.
Transaction boundaries are an architectural decision, not an implementation detail
@Transactional is implemented via an AOP proxy Spring wraps around the bean — this is why the boundary must be a public method called from outside the class. A service method calling another @Transactional method on this (self-invocation) never passes through the proxy, so the inner method silently runs with no transaction at all — no exception, just wrong behavior. This is a direct consequence of choosing proxy-based AOP as the architecture, not a bug to work around case-by-case: structure services so the transactional entry point is always called from a different bean (typically the controller, or an orchestrating service), never from a sibling method in the same class.
Package structure
Layering (controller → service → repository) is a decision about what depends on what; package structure is a separate decision about where files physically live, and the two are easy to conflate. Two conventions cover almost every real project:
Package-by-layer — one package per architectural layer, features mixed together inside each: