- Chapter
- Chapter 1 — Spring MVC Architecture
- Prerequisites
- Volumes I–III; basic HTTP
- You will be able to
- Explain the MVC pattern, identify model, view and controller in server-side and REST applications, and design controllers that stay thin
- Covers
- Model · View · Controller
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally
- 4A. Deep Dive — Keeping Controllers Thin
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
Model–View–Controller (MVC) is an architectural pattern that separates an interactive application into three responsibilities:

| Role | Responsibility | Spring MVC realisation |
|---|---|---|
| Model | Data needed to produce the response | Model/ModelMap attributes, ModelAndView, response DTOs |
| View | Rendering the model into an output format | Template views (Thymeleaf, FreeMarker, JSP) or serialised representations (JSON via message converters) |
| Controller | Handling input, invoking application logic, choosing the view/response | @Controller / @RestController classes with handler methods |
2Why It Exists
MVC originated in Smalltalk-80 (Trygve Reenskaug, 1979) to decouple user interface presentation from application data. Web frameworks adopted a variant ("Model 2") in which a controller servlet processes requests and forwards to a view page. Spring MVC (2003+) implemented Model 2 with a front controller (DispatcherServlet) and made every part pluggable. The separation remains valuable: views can change without touching business logic, the same logic can serve HTML and JSON, and each part can be tested independently.
3Core Concepts
3.1 Model
@Controller
@RequestMapping("/orders")
class OrderPageController {
private final OrderQueries orders;
OrderPageController(OrderQueries orders) { this.orders = orders; }
@GetMapping
String list(@RequestParam(defaultValue = "OPEN") OrderStatus status, Model model) {
model.addAttribute("orders", orders.byStatus(status)); // model attribute
model.addAttribute("status", status);
return "orders/list"; // view name
}
}
| Model form | Typical use |
|---|---|
Model / ModelMap parameter | Add attributes in page controllers |
ModelAndView return value | View name and model returned together |
@ModelAttribute method | Attributes common to all handlers of a controller (e.g. reference data) |
@ModelAttribute parameter | Bind request parameters (forms) into an object |
| Response DTO (REST) | The data the client receives; serialised as JSON |
3.2 View

- Template views: a
ViewResolvermaps the returned view name ("orders/list") to a template (templates/orders/list.htmlwith Thymeleaf); the view renders model attributes into HTML. - Representations: with
@ResponseBody(or@RestController), the return value itself is written by anHttpMessageConverter— the "view" is the JSON or XML representation, chosen by content negotiation.
<!-- templates/orders/list.html (Thymeleaf) -->
<tr th:each="o : ${orders}">
<td th:text="${o.id}">o-1</td>
<td th:text="${#numbers.formatDecimal(o.total, 1, 2)}">10.00</td>
</tr>3.3 Controller
@RestController
@RequestMapping("/api/orders")
class OrderApiController {
private final PlaceOrderUseCase placeOrder;
OrderApiController(PlaceOrderUseCase placeOrder) { this.placeOrder = placeOrder; }
@PostMapping
ResponseEntity<OrderView> place(@Valid @RequestBody PlaceOrderRequest req) {
Order order = placeOrder.execute(req.toCommand()); // delegate to the use case
return ResponseEntity.created(URI.create("/api/orders/" + order.id()))
.body(OrderView.from(order)); // model → representation
}
}
4How It Works Internally
1. The DispatcherServlet (Topic 1.2) receives every request and asks its handler mappings for a handler — typically a HandlerMethod wrapping the controller method.
2. A handler adapter (Topic 1.4) resolves the method's arguments, including a Model instance backed by a ModelAndViewContainer.
3. The controller executes and returns a view name, ModelAndView, or a body object.
4. For views, the DispatcherServlet asks ViewResolvers for a View and calls render(model, request, response); for bodies, a return value handler writes the object with a message converter and marks the request as handled (no view).
5. The model lives only for the duration of the request (unless flash attributes or session attributes are used).
4ADeep Dive — Keeping Controllers Thin
Controllers tend to accumulate logic: validation rules, orchestration, transaction boundaries, mapping, error translation. That makes them hard to test and couples business rules to HTTP.
| Concern | Belongs in | Why |
|---|---|---|
| HTTP binding, status codes, headers | Controller | It is the HTTP adapter |
Input shape validation (@NotBlank, sizes) | Request DTO + @Valid | Declarative, at the edge |
| Business rules and invariants | Domain / application service | Reusable, testable without HTTP |
| Transactions | Application service (@Transactional) | Use-case boundary (Volume II) |
| Mapping entity ↔ DTO | Dedicated mapper or DTO factory methods | Keeps controller readable |
| Exception → HTTP status | @ControllerAdvice (Topic 3.4) | Consistent error contract |
// Too fat: rules, persistence and HTTP mixed
@PostMapping
ResponseEntity<?> place(@RequestBody Map<String, Object> body) {
if (((List<?>) body.get("lines")).isEmpty()) {
return ResponseEntity.badRequest().build();
}
var order = new OrderEntity(); /* ... 40 lines of mapping and rules ... */
repository.save(order);
mailer.sendConfirmation(order);
return ResponseEntity.ok(order); // entity leaked to the client
}5APIs / Features / Syntax
| API | Purpose |
|---|---|
@Controller, @RestController | Controller stereotypes |
Model, ModelMap, ModelAndView | Model for views |
@ModelAttribute, @SessionAttributes, RedirectAttributes | Model binding, conversational state, flash attributes |
ViewResolver, View | View resolution and rendering |
@ResponseBody, ResponseEntity | Representation responses |
spring-boot-starter-thymeleaf | Server-side templates |
6Practical Examples
6.1 Post/Redirect/Get with flash attributes
@PostMapping("/orders")
String submit(@Valid @ModelAttribute OrderForm form, BindingResult errors,
RedirectAttributes redirect) {
if (errors.hasErrors()) return "orders/new"; // redisplay form
Order o = placeOrder.execute(form.toCommand());
redirect.addFlashAttribute("message", "Order " + o.id() + " placed");
return "redirect:/orders/" + o.id(); // avoid double submit
}6.2 Same use case, two views
@GetMapping(value = "/orders/{id}", produces = MediaType.TEXT_HTML_VALUE)
String page(@PathVariable String id, Model model) {
model.addAttribute("order", queries.byId(id));
return "orders/detail";
}
@GetMapping(value = "/orders/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody OrderView json(@PathVariable String id) { return queries.byId(id); }6.3 Common model attributes
@ModelAttribute("statuses")
// available to every view of this controller
OrderStatus[] statuses() { return OrderStatus.values(); }6.4 Testing a page controller
@WebMvcTest(OrderPageController.class)
class OrderPageControllerTest {
@Autowired MockMvcTester mvc;
@MockitoBean OrderQueries orders;
@Test
void rendersList() {
given(orders.byStatus(OrderStatus.OPEN)).willReturn(List.of(sample()));
assertThat(mvc.get().uri("/orders")).hasStatusOk()
.hasViewName("orders/list").model().containsKey("orders");
}
}6.5 View technologies at a glance
| Technology | Starter | Characteristics |
|---|---|---|
| Thymeleaf | spring-boot-starter-thymeleaf | Natural HTML templates, strong Spring integration |
| FreeMarker | spring-boot-starter-freemarker | Mature template language, also for emails |
| Mustache | spring-boot-starter-mustache | Logic-less, simple |
| JSP | (servlet container support) | Legacy; not supported with executable jars — avoid for new apps |
| JSON/XML representations | spring-boot-starter-webmvc + Jackson | REST APIs; view = serialised model |
6.6 Classic MVC versus MVC in a REST API
| Aspect | Classic (server-side) | REST API |
|---|---|---|
| Controller output | View name + model | Object / ResponseEntity |
| View selection | ViewResolver | Content negotiation + message converters |
| Model lifetime | Request (+ flash/session) | Request only |
| State | Often session-based (login, forms) | Stateless, token-based |
| Error presentation | Error pages / form errors | Problem Details JSON |
7Common Problems & Mistakes
- Business logic in controllers.
- Entities as models or response bodies.
- Stateful controllers (fields holding request data) — controllers are singletons.
- Mixing HTML and JSON concerns in one handler without
produces. - Forgetting Post/Redirect/Get — duplicate form submissions on refresh.
8Performance & Production Considerations
| Concern | Guidance |
|---|---|
| Template rendering | Enable template caching in production (default) |
| Model size | Only what the view needs — large models cost memory and render time |
| DTO mapping | Cheap compared with I/O; avoid reflection-heavy mapping in hot paths |
| Session attributes | Increase memory per user; avoid for stateless APIs |
9Architecture & Design Considerations
- Controllers are adapters in hexagonal architecture: HTTP in, use case call, HTTP out.
- Separate API DTOs from domain and persistence models.
- Choose server-side rendering vs SPA + API per product needs; both run on the same MVC core.
- Consistent controller conventions across teams (naming, DTOs, error handling).
10Interview Questions & Answers
What does MVC stand for?
MVC stands for Model–View–Controller, a pattern for separating responsibilities in applications with a user interface. The model holds the data and state to be displayed, the view renders that model into a representation such as an HTML page or a JSON document, and the controller receives user input or HTTP requests, calls the application's business logic, and decides which model and view to use. In Spring MVC, the DispatcherServlet routes each request to a controller method, and the result is rendered by a view or converted to JSON. The separation keeps presentation, request handling and business logic independently changeable.
What is the "view" in a REST API built with Spring?
In a REST API there is no HTML template; the view is the serialised representation of the object the controller returns. When a @RestController method returns an OrderResponse, Spring selects an HttpMessageConverter based on the request's Accept header, usually the Jackson converter, and writes the object to the response body as JSON. Content negotiation can produce other formats, such as XML, from the same model. This means the shape of returned objects is the API contract, so it should be designed deliberately with dedicated response DTOs, rather than exposing whatever internal objects happen to be convenient.
How does a controller return a server-rendered page?
A controller annotated with @Controller adds data to a Model object, for example model.addAttribute("orders", orders), and returns a logical view name such as "orders/list". Spring's ViewResolver maps that name to a template, for example a Thymeleaf file at templates/orders/list.html, and the template engine renders HTML using the model attributes. Because the controller returns only a name, the template technology can change without changing the controller. Forms work the other way: submitted fields are bound to an object annotated with @ModelAttribute, validated with @Valid, and errors are shown in the same template.
Why should controllers not return JPA entities?
Returning JPA entities couples your API to your database schema: any change to the entity, such as renaming a field or adding a relationship, changes the JSON clients receive. It can expose internal fields such as audit columns or passwords. Serialising an entity may trigger lazy loading of associations, causing extra queries, LazyInitializationException errors or very large responses through bidirectional relationships. It also tempts clients to send entities back, which allows them to change fields they should not. Use dedicated request and response DTOs, often records, and map explicitly; this keeps the API stable while the persistence model evolves.
What is Post/Redirect/Get and why use it?
Post/Redirect/Get is a pattern for handling form submissions in server-rendered applications. After a successful POST, such as placing an order, the controller does not render a page directly; it returns a redirect, for example redirect:/orders/42, and the browser then issues a GET to that URL. Messages such as "Order placed" can be passed with flash attributes, which survive exactly one redirect. If the user refreshes the resulting page, the browser repeats the harmless GET instead of resubmitting the form, which avoids duplicate orders or payments. It also gives each result page a bookmarkable URL.
Where should validation and business rules live?
Separate the two kinds of rule. Validation of the input's shape belongs at the boundary: required fields, formats, lengths and ranges, declared with Bean Validation annotations on request DTOs and triggered with @Valid in the controller, so bad requests are rejected early with a 400 response. Business rules, such as whether a customer may place an order or whether stock is available, belong in the domain or application service layer. They must apply no matter how the service is called, whether from HTTP, messaging or a batch job, and they should be testable without a web container.
How does MVC relate to hexagonal architecture?
In hexagonal architecture, the application core, with its domain model and use cases, sits in the middle and talks to the outside world through ports. Adapters connect those ports to specific technologies. A Spring MVC controller is an inbound adapter: it translates an HTTP request into a call on a use case, such as placeOrder, and translates the result back into an HTTP response. It should contain no business logic. Keeping controllers this thin means the core does not depend on the web framework, so the same use cases can also be driven by message listeners, batch jobs or tests.
When would you choose server-side rendering over an SPA with a REST API?
Choose by the kind of application and the team. Server-side rendering, with Spring MVC and Thymeleaf or a similar engine, suits content-heavy and form-heavy applications with modest interactivity, applications where search-engine visibility matters, and small teams who benefit from one code base and simple deployment. A single-page application calling a REST API suits highly interactive user interfaces, offline capabilities, and situations where several clients, such as web and mobile apps or partners, share the same API. Many systems mix them, and approaches such as HTMX add interactivity to server-rendered pages without a full SPA.