Spring Boot
Model
Model is the Spring MVC interface for passing data from a controller to a view template. When a @Controller method renders a Thymeleaf or JSP template, it populates a Model with named attributes — objects, lists, strings, flags — that the template can access by name. Model, ModelMap, and ModelAndView are the three ways to work with model data in Spring MVC.
What the Model Does in Spring MVC
Spring MVC separates deciding what to show from showing it. The controller method prepares the data, and the view template (Thymeleaf or JSP) turns that data into HTML. The Model is the hand-off between them: a map of named attributes that the template reads by name.
For each request, DispatcherServlet calls your controller method, collects the attributes you added to the model together with the view name you returned, and passes both to the ViewResolver and the view. The attributes exist only for that one request. Once the response has been sent they are gone, which is why data that must survive a redirect needs RedirectAttributes, covered below.
Spring MVC lets you work with the model in several forms: a Model parameter, a ModelMap, a plain Map, or a ModelAndView return value. They all end up in the same place, so the choice is mostly style. This page focuses on Model, the form most controllers use.
Model, ModelMap, and @ModelAttribute
Model is an interface with a small, clear API: addAttribute(), addAllAttributes(), containsAttribute() and asMap(). Spring injects it into any handler method that declares it as a parameter. At runtime the object is an ExtendedModelMap, which is a ModelMap that implements Model, so the options below all write to the same underlying map.
ModelMap is a Map-based class with the same addAttribute() methods, and you can also declare a plain Map<String, Object> parameter. Prefer Model: it is the type most Spring code and documentation use, it hides the Map details, and it reads the same in every controller.
If you call addAttribute(value) without a name, Spring generates one from the type of the value. For a List of User objects the name is userList, not users. Passing an explicit name avoids surprises in the template.
A method annotated with @ModelAttribute runs before every handler method in its controller and adds its return value to the model. This suits small shared values such as an application name or the signed-in user. Because it runs on every request to that controller, keep it cheap. Put it in a @ControllerAdvice class if the attribute should be available to all controllers.
Java
@Controller
@RequestMapping("/users")
public class UserPageController {
private final UserService userService;
// ── Model — injected directly as a method parameter: ──────────────
@GetMapping
public String listUsers(Model model) {
model.addAttribute("users", userService.findAll());
model.addAttribute("pageTitle", "All Users");
model.addAttribute("totalCount", userService.count());
return "users/list"; // view name → templates/users/list.html
}
// ── ModelMap — a Map-based alternative, same result: ─────────────
@GetMapping("/{id}")
public String userDetail(@PathVariable Long id, ModelMap modelMap) {
modelMap.addAttribute("user", userService.findById(id));
modelMap.addAttribute("orders", orderService.findByUser(id));
return "users/detail";
}
// ── Map<String, Object> — simplest form, works identically: ───────
@GetMapping("/dashboard")
public String dashboard(Map<String, Object> model) {
model.put("stats", userService.getStats());
model.put("recentUsers", userService.findRecent(10));
return "users/dashboard";
}
// ── @ModelAttribute on a method — adds attribute to ALL handlers: ─
// Runs before every handler method in this controller:
@ModelAttribute("appName")
public String appName() {
return "MyApplication"; // available as ${appName} in every template
}
@ModelAttribute("currentUser")
public UserResponse currentUser(Principal principal) {
return userService.findByUsername(principal.getName());
}
}Passing Data to Templates
Every model attribute becomes a variable in the template context. In Thymeleaf you read it with a dollar-brace expression such as ${search}, and the name in the template must match the name used in the controller exactly, including case. The controller should choose clear, consistent names because the template depends on them.
Pass the template only what it needs. Simple values such as strings, numbers and booleans are easy to display, and a small DTO or a list of DTOs keeps the view independent of your database entities. Passing JPA entities with lazy associations can trigger extra queries while the template is rendering, which is hard to spot. The pagination example below passes the page number, page count and next and previous flags as separate attributes so the template needs no extra logic.
Be aware of how missing values behave. Reading an attribute that was never added returns null, but reading a property on that null value, such as user.name, fails with an error. Either make sure the controller always adds what the template uses, or guard the expression with th:if or the safe-navigation operator (user?.name).
Java
// ── Controller: ───────────────────────────────────────────────────────
@GetMapping
public String listUsers(
Model model,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "") String search) {
Page<UserResponse> users = userService.search(search, PageRequest.of(page, 20));
model.addAttribute("users", users.getContent());
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", users.getTotalPages());
model.addAttribute("totalElements", users.getTotalElements());
model.addAttribute("search", search);
model.addAttribute("hasNext", users.hasNext());
model.addAttribute("hasPrevious", users.hasPrevious());
return "users/list";
}
<!-- Template — accessing model attributes: -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<input type="text" th:value="${search}" name="search" />
<p th:text="|Showing ${users.size()} of ${totalElements} users|"></p>
<tr th:each="user : ${users}">
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
</tr>
<a th:if="${hasPrevious}"
th:href="@{/users(page=${currentPage - 1}, search=${search})}">Previous</a>
<a th:if="${hasNext}"
th:href="@{/users(page=${currentPage + 1}, search=${search})}">Next</a>
</body>
</html>@ModelAttribute on Method Parameters
@ModelAttribute on a method parameter tells Spring to build a Java object from the request. Spring finds or creates the object, then binds form fields, query parameters and path variables onto its properties by name. This is the standard way to receive an HTML form in Spring MVC. A parameter of a non-simple type is treated as a model attribute even if you leave the annotation off, and the attribute name defaults to the decapitalized class name, such as createUserForm, unless you give one explicitly as the example does.
A form needs two handler methods. The GET method puts an empty form object into the model, which the template binds to (th:object in Thymeleaf). The POST method receives the submitted object. Add @Valid to run bean validation, and place a BindingResult parameter immediately after the validated argument. If validation fails, return the form view again: the object still holds what the user typed, and the errors are available to the template.
Use a dedicated form class rather than binding straight onto a JPA entity. Binding copies any matching request parameter onto the object, so an entity can end up with fields set that the form never offered (mass assignment). A small form class lists exactly which fields the user may submit.
Java
// ── Form command object — fields match form input names: ─────────────
public class CreateUserForm {
@NotBlank private String name;
@NotBlank @Email private String email;
private User.Role role = User.Role.USER;
// getters + setters (needed for setter-based binding)
}
@Controller
@RequestMapping("/users")
public class UserFormController {
// ── GET — display the empty form: ─────────────────────────────────
@GetMapping("/new")
public String newUserForm(Model model) {
model.addAttribute("userForm", new CreateUserForm()); // empty form object
model.addAttribute("roles", User.Role.values());
return "users/form";
}
// ── POST — receive the submitted form: ────────────────────────────
@PostMapping
public String createUser(
@ModelAttribute("userForm") @Valid CreateUserForm form,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
// Re-render the form — form object retains the invalid values:
model.addAttribute("roles", User.Role.values());
return "users/form"; // back to the form template
}
userService.create(form);
return "redirect:/users"; // PRG pattern — redirect after success
}
}
// ── How Spring resolves a @ModelAttribute argument: ───────────────────
// 1. If the model already holds an attribute with that name (from a
// @ModelAttribute method, a flash attribute or @SessionAttributes),
// that object is used. Otherwise a new instance is created.
// 2. Request parameters and URI path variables are then bound onto it
// by name, overwriting matching properties.RedirectAttributes — Passing Data Across Redirects
After a successful POST, redirect to a GET page instead of rendering a view directly. This Post/Redirect/Get pattern stops the browser from resubmitting the form if the user refreshes the page. The catch is that a redirect makes the browser send a brand-new request, so the model from the POST is gone.
RedirectAttributes solves this with two kinds of data. A flash attribute, added with addFlashAttribute(), is stored temporarily (in the session by default), becomes part of the model of the next request, and is then removed. It is the right choice for a one-time message such as "User created successfully", and it does not appear in the URL. A normal attribute, added with addAttribute(), is appended to the redirect URL as a query parameter, so use it only for simple, non-sensitive values such as an id.
Because a flash attribute is removed after it is read, refreshing the target page hides the message again. That is the intended behaviour for notifications.
Java
@Controller
@RequestMapping("/users")
public class UserFormController {
// ── POST → redirect → GET (Post/Redirect/Get pattern): ────────────
@PostMapping
public String create(
@ModelAttribute @Valid CreateUserForm form,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "users/form";
}
UserResponse created = userService.create(form);
// Flash attribute — survives one redirect, then removed:
redirectAttributes.addFlashAttribute("successMessage",
"User '" + created.name() + "' created successfully");
// Regular redirect attribute — appended to the URL as query param:
redirectAttributes.addAttribute("highlight", created.id());
return "redirect:/users";
// Redirects to: /users?highlight=43
// Flash: successMessage available in /users model, then gone
}
}
// ── Receiving in the redirect target: ─────────────────────────────────
@GetMapping
public String listUsers(Model model,
@RequestParam(required = false) Long highlight) {
model.addAttribute("users", userService.findAll());
model.addAttribute("highlight", highlight);
// successMessage is already in the model automatically from flash storage
return "users/list";
}
<!-- Template — display the flash message: -->
<div th:if="${successMessage}" class="alert alert-success">
<p th:text="${successMessage}"></p>
</div>
<tr th:each="user : ${users}"
th:classappend="${user.id == highlight} ? 'highlighted' : ''">
...
</tr>Testing Model Attributes
Controller tests can check the model directly. With MockMvc in a @WebMvcTest test, model() asserts on the attributes, view() on the view name, and flash() on flash attributes, so you can verify a page without rendering the template. The tests below assume the controllers shown above and a mocked UserService. The second test covers the validation path: the form view is shown again and the model reports errors on the invalid fields. The third checks that a successful submit redirects and sets the flash message.
Java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@Test
void listPageHasUsersAndTitle() throws Exception {
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(view().name("users/list"))
.andExpect(model().attributeExists("users"))
.andExpect(model().attribute("pageTitle", "All Users"));
}
@Test
void invalidFormRedisplaysFormWithErrors() throws Exception {
mockMvc.perform(post("/users")
.param("name", "")
.param("email", "not-an-email"))
.andExpect(status().isOk())
.andExpect(view().name("users/form"))
.andExpect(model().attributeHasFieldErrors("userForm", "name", "email"));
}
@Test
void validFormRedirectsWithFlashMessage() throws Exception {
mockMvc.perform(post("/users")
.param("name", "Asha")
.param("email", "asha@example.com"))
.andExpect(status().is3xxRedirection())
.andExpect(flash().attributeExists("successMessage"));
}Common Mistakes with the Model
- Expecting model data to survive a redirect. The model belongs to a single request, so use addFlashAttribute() for messages and addAttribute() for simple values that belong in the URL.
- Using different names in the controller and the template. If the controller adds users but the template reads userList, the page renders empty. Keep names identical and explicit.
- Forgetting the empty form object on the GET request. A Thymeleaf form with th:object then fails with an error like: Neither BindingResult nor plain target object for bean name 'userForm' available as request attribute.
- Placing BindingResult anywhere except directly after the @Valid argument. Validation errors then cause a 400 response instead of re-displaying the form.
- Doing heavy work in a @ModelAttribute method. It runs on every request to the controller, including pages that never use the value.
- Binding request parameters directly onto JPA entities. Use a form class so users cannot set fields you did not intend.