Intro
When you click a button in a browser, a lot happens before any data comes back. This post walks throught the full lifecycle of an HTTP request inside a Spring MVC application.
Index
- The HTTP Request Structure
- DispatcherServlet: The Front Controller
- Layered Architecture: Controller / Service / Repository
- Annotations: Lables, Not Logic
- IoC and Dependency Injection
- Bean Scopes
- Full Request Lifecycle
The HTTP Request Structure
Every interaction between a client and a Spring server is an HTTP message. A typical POST request looks like this:
POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"name": "Alice",
"email": "alice@test.com"
}- Method - the operation type (GET, POST, PUT, PATCH, DELETE)
- Path - the resource being targeted
- Headers - metadata about the request
- Body - the payload
Understanding this structure is a prerequisite for understanding @RequestParam, @PathVariable, and @RequestBody. Each annotation corresponds to a different part of this structure: query string, path segment, and body, respectively.
DispatcherServlet: The Front Controller
Spring MVC is built around the Front Controller pattern. Everything incoming HTTP request is routed through a single entry point: DispatcherServlet.
- Recieves the raw HTTP request
- Delegates to
HandlerAdapterto call that method with resolved arguments - Invokes
HandlerAdapterto call that method with resolved arguments - Passes the return value through
HttpMessageConverter(Jackson by default) to produce the response body
You never instantiate DispatcherServlet directly. Spring Boot auto-configures it via DispatcherServletAutoConfiguration.
Client -> DispatcherServlet -> HandlerMapping -> Controller -> MessageConverter -> ClientThe reason it exists is so your controllers don't have to worry about any of that plumbing.
- Receives every single incoming request — it's the one entry point, no exceptions
- Finds the right controller method — looks at the URL and HTTP method, matches it to something like
@PostMapping("/api/members") - Wires everything together — converts the JSON body into a Java object, calls your method, then converts the return value back to JSON for the response
Layered Architecture: Controller/Service/Repository
Dumpling everything into a controller works until it doesn’t.
I talked about layered architecture in my blog.
MSA - Ria Choi
Annotations: Labels, Not Logic
Annotations don’t execute anything. They are metadata that Spring reads at startup(or runtime) to decide how to configure and wire components.

@RestController is a composed annotation. It includes both @Controller and @ResponseBody. That’s why the class gets registered as a Spring bean and why return values get serialized to JSON automatically.
@Transactional works through a proxy: Spring wraps your service in a subclass that intercepts calls and manages the transaction boundary. This is why @Transactional has no effect on private methods. The proxy can’t intercept what it can’t override.
IoC and Dependency Injection
Traditionally, objects create their own dependencies.
MemberRepository repository = new MemberRepository();
MemberService service = new MemberService(repository);Spring inverts this. The IoC container (specifically ApplicationContext) is responsible for creating and writing beans. You declare what a class needs. Spring figures out how to satisfy it.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}@RequiredArgsConstructor (Lombok) generates constructor or all final fields. Spring detects the single conrstuctor and uses it for constructor injection. The preferred injection style because it makes dependencies explicit and the class easy to test.
The objects managed by the container are called beans. Any class annotated with @Component, @Controller, @Service, or @Repository becomes a bean candidate, scanned at startup.
Bean Scopes
Scopes control how many instances of a bean exist and for how long.

singeton is the default because most service and repository classes are stateless. They hold no per-request data. One instance shared all callers is safe and efficient.
Storing mutable state in a singleton field.
@Service
public class UserService {
private String currentUser;
} Multiple concurrent requests share the same UserService instance. A field mutation in one request is visible to all others. Keep singleton beans stateless; pass any per-request state through method parameters.
Full Request Lifecycle
Putting it all together for POST/api/members
1. Client sends HTTP POST with JSON body
2. DispatcherServlet receives the request
3. HandlerMapping matches the path + method to MemberController.create()
4. @RequestBody triggers Jackson to deserialize the body into MemberDto
5. Controller calls memberService.create(dto)
6. @Transactional proxy opens a transaction
7. Service calls memberRepository.save(member)
8. JPA translates the call to SQL; database executes the INSERT
9. The saved entity travels back up the call stack
10. @Transactional proxy commits the transaction
11. Controller returns the entity; MessageConverter serializes it to JSON
12. DispatcherServlet writes the HTTP 201 responseEach layer has one job.
The controller doesn't know SQL.
The repository doesn't know business rules.
The service doesn't know HTTP.
That separation is what makes the system testable and maintainable at scale.

