REST API · Lesson 28 of 95
Spring MVC Architecture
Learn Spring MVC architecture: how the DispatcherServlet, HandlerMapping, controller, service and JSON converter handle one web request in Spring Boot.
Walk into a busy library. A helper at the front desk listens to what you need. She does not fetch the book herself. She sends you to the right shelf, a librarian finds it in the stacks, and the desk hands it back to you neatly wrapped. Nobody in that library does everyone's job, and that is why it runs smoothly.
Spring MVC architecture works the same way for web requests. Each class has one job, and a front desk sends every request to the right place. Let's see how it is built and how one request travels through it.
What is Spring MVC?
MVC splits a web app into three roles.
- Model is the data. In a library app, that is a book with a title and an author.
- View is what the user sees. In a website it is an HTML page. In a REST API the "view" is the JSON in the response.
- Controller is the traffic officer. It receives the request, asks for the data, and picks what to send back.
Spring MVC adds one more player: the DispatcherServlet. It is the front desk of the library. Every request enters through it, so you never have to write code that reads raw URLs and headers yourself.
In Spring Boot 4, the starter spring-boot-starter-webmvc brings Spring MVC, the JSON library and an embedded Tomcat server. Older Boot 3 projects called this starter spring-boot-starter-web.
Why is it used?
Without a framework, you would write code to read the URL, find the method, convert text to numbers, build the response and set headers. You would repeat this for every endpoint. Spring MVC does the boring work for you.
- One entry point. Every request passes through the DispatcherServlet, so common work like error handling is done in one place.
- Clean separation. Web code, business rules and data code live in different classes. You can change one without breaking the others.
- Automatic conversion. A path like
/books/7becomes a Java number, and a returned object becomes JSON. - Easy testing. A controller that only handles web details is small and simple to test.
How it works
Every request follows the same journey. Here it is for a call to GET /books.
textBrowser or curl | GET /books v +---------------------------+ | Tomcat (embedded server) | +---------------------------+ | v +---------------------------+ | DispatcherServlet | | (the front desk) | +---------------------------+ | "who handles this?" v +---------------------------+ | HandlerMapping | | finds BookController.list | +---------------------------+ | v +---------------------------+ | BookController.list() | | calls BookService | +---------------------------+
Tomcat accepts the connection and passes the request to the DispatcherServlet. The DispatcherServlet asks the HandlerMapping, which is like a phone directory, which method owns GET /books. Then it calls that controller method. The controller asks the service for data. This is the trip in.
Now the trip back out.
textBookService returns List<Book> | v +---------------------------+ | BookController returns | | the list | +---------------------------+ | v +---------------------------+ | Message converter | | (Jackson) makes JSON | +---------------------------+ | v +---------------------------+ | Response: 200 OK + JSON | +---------------------------+
The controller returns plain Java objects. Because the class is a @RestController, Spring hands the result to a message converter. Jackson writes it as JSON, and Tomcat sends it to the client. In a classic website, a view resolver would pick an HTML template here instead.
Real-Life Example
Think of a restaurant. The guest is the browser. The waiter at the door is the DispatcherServlet. He listens to the order and walks it to the right counter, which is the controller. The counter passes the details to the kitchen, which is the service. The kitchen uses the pantry, which is the data. The plated dish comes back the same way and reaches the guest. The waiter never cooks, and the cook never greets guests.
Code Example
Let's build a small book API for a library called PageTurn. We have three classes with three jobs: a model, a service and a controller. A few System.out.println lines will show the order of the calls.
textpage-turn/ ├─ pom.xml └─ src/main/java/ └─ com/pageturn/library/ ├─ LibraryApplication.java ├─ Book.java ├─ BookService.java └─ BookController.java
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.pageturn</groupId> <artifactId>library</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: LibraryApplication.java in package com.pageturn.library
javapackage com.pageturn.library; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class LibraryApplication { public static void main(String[] args) { SpringApplication.run(LibraryApplication.class, args); } }
File: Book.java in package com.pageturn.library
javapackage com.pageturn.library; public record Book(int id, String title, String author) {}
File: BookService.java in package com.pageturn.library
javapackage com.pageturn.library; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Service; @Service public class BookService { private final List<Book> shelf = List.of( new Book(1, "The River Clock", "Meera Joshi"), new Book(2, "Salt and Stars", "Arjun Rao")); public List<Book> findAll() { System.out.println("Service: reading the shelf"); return shelf; } public Optional<Book> findById(int id) { return shelf.stream().filter(b -> b.id() == id).findFirst(); } }
File: BookController.java in package com.pageturn.library
javapackage com.pageturn.library; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BookController { private final BookService service; public BookController(BookService service) { this.service = service; } @GetMapping("/books") public List<Book> list() { System.out.println("Controller: request received"); return service.findAll(); } }
Start the app, then call the endpoint from a second terminal:
bashmvn spring-boot:run curl http://localhost:8080/books
Output:
json[ { "id": 1, "title": "The River Clock", "author": "Meera Joshi" }, { "id": 2, "title": "Salt and Stars", "author": "Arjun Rao" } ]
The console of the running app shows the order of the calls:
textController: request received Service: reading the shelf
The JSON is spaced out here for reading; curl prints it on one line.
Code Explained
Bookis the model. It is a plain record that only holds data.BookServiceis marked@Service. It owns the data and the rules. The controller never touches the list directly.BookControlleris marked@RestController. It receives the request and hands the work to the service.- The constructor asks for a
BookService. Spring creates the service and passes it in. This is called constructor injection. @GetMapping("/books")tells the HandlerMapping that this method answersGET /books.- The controller returns a
List<Book>. There is no JSON code anywhere, because Jackson writes it for us. - The two
printlnlines prove the order: the controller runs first, then the service.
Spring MVC Architecture Layers at a Glance
| Layer | Job | Example class |
|---|---|---|
| Controller | Reads the request, returns the response | BookController |
| Service | Holds business rules | BookService |
| Model | Carries the data | Book |
| Front controller | Routes every request | DispatcherServlet |
Common Mistakes
- Controller outside the scan area. If
BookControllersits in a package that is not under the main class's package, Spring never finds it, and every call returns 404. - Talking about the DispatcherServlet as your code. You do not create or configure it. Spring Boot registers it for you.
- Mixing up model and entity. A model here is any class that carries data. It does not have to be a database table.
Interview Questions
What is the DispatcherServlet?
Ans:It is the front controller of Spring MVC. Every request enters through it, and it finds the right controller method and sends back the response.
What does the HandlerMapping do?
Ans:It matches the request path and HTTP method to a controller method, like a phone directory for endpoints.
What is the difference between MVC in a website and in a REST API?
Ans:In a website the view is an HTML page from a template. In a REST API the view is the response body, usually JSON, written by a message converter.
Why keep controllers thin?
Ans:Thin controllers are easy to read and test, and the rules in services can be reused from many endpoints.
Key Points to Remember
- MVC means Model, View and Controller. Each has one job.
- The DispatcherServlet is the single front door for all requests.
- The HandlerMapping picks the controller method for a request.
- In a REST API, message converters such as Jackson write the response body.
- Controllers handle web details; services hold business rules.
- Spring Boot sets up all this machinery when you add
spring-boot-starter-webmvc.
Frequently Asked Questions
Do I have to write the DispatcherServlet myself?
No. Spring Boot creates and registers it automatically. You only write controllers and services.
Is Spring MVC only for websites with HTML pages?
No. It also powers REST APIs. The same request journey applies, except the response body is JSON instead of a page.
What is the difference between Spring MVC and Spring WebFlux?
Spring MVC uses one thread per request and blocking style code. WebFlux is a reactive alternative for non-blocking code. Most beginners and most projects use MVC.
Where does the service layer fit in Spring MVC architecture?
MVC describes the web side only. The service layer sits behind the controller and keeps business rules out of it.
Related Topics
- @Controller vs @RestController: choose between HTML pages and JSON responses.
- @RequestMapping: map paths and methods to controller code.
- GET POST PUT DELETE Mapping: use the shortcut annotations for each HTTP method.
- Building a Complete CRUD REST API: put controller, service and data together.
Practice Problems
Try each problem on your own first. Both use the same pom.xml as the PageTurn example; only change the groupId and artifactId.
Easy: Clinic Doctors on Duty
Sunrise Clinic wants a small API for its waiting room screen. Build three classes with three jobs: a Doctor record with name and specialty, a DoctorService that holds the list, and a DoctorController that answers GET /doctors. Use two doctors: Dr. Nisha Patil (Pediatrics) and Dr. Rohan Kulkarni (Orthopedics).
Show answerHide answer
File: ClinicApplication.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ClinicApplication { public static void main(String[] args) { SpringApplication.run(ClinicApplication.class, args); } }
File: Doctor.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; public record Doctor(String name, String specialty) {}
File: DoctorService.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; import java.util.List; import org.springframework.stereotype.Service; @Service public class DoctorService { public List<Doctor> findAll() { return List.of( new Doctor("Dr. Nisha Patil", "Pediatrics"), new Doctor("Dr. Rohan Kulkarni", "Orthopedics")); } }
File: DoctorController.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class DoctorController { private final DoctorService service; public DoctorController(DoctorService service) { this.service = service; } @GetMapping("/doctors") public List<Doctor> list() { return service.findAll(); } }
Run mvn spring-boot:run, then curl http://localhost:8080/doctors prints:
json[ { "name": "Dr. Nisha Patil", "specialty": "Pediatrics" }, { "name": "Dr. Rohan Kulkarni", "specialty": "Orthopedics" } ]
curl prints the JSON on one line; it is spaced out here so it is easy to read.
Medium: Cinema Shows with Three Layers
StarPlex Cinema wants its show data kept in a separate repository class. Build a controller, a service and a repository:
ShowRepositoryholds the list of shows (a record withmovieandseatsLeft).ShowServicehasfindAll()andcountSoldOut(), which counts shows with zero seats left.ShowControllerservesGET /showsandGET /shows/sold-out-count.
Use these shows: Monsoon Express (0 seats), The Silent Orbit (42 seats), Night Market (0 seats).
Show answerHide answer
File: StarplexApplication.java in package com.starplex.shows
javapackage com.starplex.shows; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class StarplexApplication { public static void main(String[] args) { SpringApplication.run(StarplexApplication.class, args); } }
File: Show.java in package com.starplex.shows
javapackage com.starplex.shows; public record Show(String movie, int seatsLeft) {}
File: ShowRepository.java in package com.starplex.shows
javapackage com.starplex.shows; import java.util.List; import org.springframework.stereotype.Repository; @Repository public class ShowRepository { public List<Show> findAll() { return List.of( new Show("Monsoon Express", 0), new Show("The Silent Orbit", 42), new Show("Night Market", 0)); } }
File: ShowService.java in package com.starplex.shows
javapackage com.starplex.shows; import java.util.List; import org.springframework.stereotype.Service; @Service public class ShowService { private final ShowRepository repository; public ShowService(ShowRepository repository) { this.repository = repository; } public List<Show> findAll() { return repository.findAll(); } public long countSoldOut() { return repository.findAll().stream() .filter(s -> s.seatsLeft() == 0) .count(); } }
File: ShowController.java in package com.starplex.shows
javapackage com.starplex.shows; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ShowController { private final ShowService service; public ShowController(ShowService service) { this.service = service; } @GetMapping("/shows") public List<Show> list() { return service.findAll(); } @GetMapping("/shows/sold-out-count") public long soldOutCount() { return service.countSoldOut(); } }
Calling the sold-out endpoint prints 2:
bashcurl http://localhost:8080/shows/sold-out-count