A tutorial for building a server-rendered web application with Spring MVC (on Spring Boot), Thymeleaf, Spring Data JPA / Hibernate and MySQL. It covers an e-commerce domain (categories, products, customers, orders) with a classic CRUD UI: list, add, edit and delete pages, Bean Validation feedback rendered inline, and flash messages after each action.
This document is the specification of the project: it is meant to be followed step by step to implement each branch.
- Tech stack
- Data model
- Branching strategy
- Project structure
- Routes
- Testing strategy
- feature/config
- feature/products
- feature/customers
- feature/orders
- Order of work
- Code conventions
- Concepts covered
- How to follow this tutorial
| Component | Choice |
|---|---|
| Framework | Spring Boot 2.7.x (Spring MVC, Spring Data JPA) |
| Language | Java 8 |
| Build | Maven (with the Maven wrapper) |
| Database | MySQL (Hibernate ddl-auto=update) |
| Template engine | Thymeleaf, with thymeleaf-layout-dialect for the shared layout |
| Front-end | Bootstrap, jQuery, Chart.js (static assets under src/main/resources/static) |
| Mapping | MapStruct (entity ↔ DTO) |
| Boilerplate reduction | Lombok |
| Validation | Jakarta/Bean Validation on entities and DTOs |
| Seed data | JavaFaker, through factories and seeders run under the dev profile |
| Tests | JUnit 5, Mockito, Spring Test (MockMvc), H2 |
categories (id, category_name)
│ 1
│
│ N
products (id, category_id, product_name, unit_price)
│ 1
│
│ N
orders (id, customer_id, product_id, qty, total)
│ N
│
│ 1
customers (id, first_name, last_name, tel, email, address)
| Branch | Role |
|---|---|
master |
Stable, production-ready code. No direct commits, only merges from develop. |
develop |
Integration branch. |
feature/config |
Project set-up: dependencies (MapStruct, Lombok, H2, test starters), datasource configuration, temporary deactivation of Spring Security. |
feature/products |
Category and Product: entity, DTO, mapper, repository, service, controller, views, tests. |
feature/customers |
Customer: same layers as above. |
feature/orders |
Order, which depends on Product and Customer. |
spring_mvc_tutorial/
├── pom.xml
├── mvnw, mvnw.cmd
└── src/
├── main/
│ ├── java/com/edgareldy/spring_mvc_tutorial/
│ │ ├── SpringMvcTutorialApplication.java
│ │ ├── controller/ (Category, Product, Customer, Order, Dashboard controllers)
│ │ ├── dto/ (CategoryDto, ProductDto, CustomerDto, OrderDto)
│ │ ├── entity/ (Category, Product, Customer, Order)
│ │ ├── mapper/ (MapStruct interfaces, entity ↔ dto)
│ │ ├── repository/ (Spring Data JPA repositories)
│ │ ├── service/ (service contracts)
│ │ │ └── impl/ (service implementations, logging and transactions)
│ │ ├── exception/ (ResourceNotFoundException, GlobalExceptionHandler)
│ │ └── database/
│ │ ├── factory/ (JavaFaker-based factories)
│ │ └── seeder/ (DatabaseSeeder and one seeder per entity, `dev` profile only)
│ └── resources/
│ ├── application.properties
│ ├── static/ (css, js, fonts)
│ └── templates/
│ ├── layouts/default.html
│ ├── partials/ (nav, sidebar)
│ ├── dashboard/, categories/, products/, customers/, orders/ (index, add, edit)
│ └── error/ (404, 500)
└── test/java/com/edgareldy/spring_mvc_tutorial/
├── SpringMvcTutorialApplicationTests.java
├── controller/ (one MockMvc test class per controller)
└── service/ (one Mockito test class per service)
Every resource (/categories, /products, /customers, /orders) exposes the same six routes:
| Method | Path | Purpose |
|---|---|---|
| GET | /<resource> |
List page |
| GET | /<resource>/add |
Empty creation form |
| POST | /<resource> |
Create, then redirect to the list with a flash message |
| GET | /<resource>/edit/{id} |
Pre-filled edit form |
| POST | /<resource>/edit/{id} |
Update, then redirect to the list with a flash message |
| POST | /<resource>/delete/{id} |
Delete, then redirect to the list with a flash message |
GET / renders the dashboard. A validation error re-renders the form with inline messages; an unknown id is mapped by GlobalExceptionHandler to the error/404 view and any other exception to error/500.
Every branch from feature/products onward ships tests at both layers before its Pull Request is opened.
| Layer | Tool | What it verifies | Data |
|---|---|---|---|
| Service | JUnit 5 + Mockito | Business rules and orchestration, with every repository (and any other service) mocked, asserting on the returned DTOs | No database, constructed test data only |
| Controller | JUnit 5 + Spring Test (MockMvc) |
HTTP-level behavior: returned view names, redirects, flash messages, model content, validation errors, with the service layer mocked | No database |
Every test method, at every layer, is named _NN_Should<Outcome>_When<Condition>: a two-digit, zero-padded sequence number (the order of the methods within the class, restarting at _01_ in each class; JUnit does not enforce it, it is kept consistent by convention), followed by what is expected, followed by the condition that produces it.
@Test
void _01_ShouldReturnCategory_WhenCategoryExists() { ... }
@Test
void _02_ShouldThrowResourceNotFound_WhenCategoryDoesNotExist() { ... }No other naming style (shouldX(), testX(), givenX_whenY_thenZ(), getCategories_returnsIndexView()) is used anywhere in this project's test suite. This applies to test methods only, not to @BeforeEach/@AfterEach helpers.
Technical foundation, to be merged first into develop.
- Initialize the project (Maven, Java 8, Spring Boot 2.7.x) with the Spring MVC, Thymeleaf, Data JPA, validation and MySQL dependencies
- Replace ModelMapper with MapStruct, and configure the Lombok + MapStruct annotation processors
- Add H2 and
spring-security-testas test dependencies -
application.properties: MySQL datasource, Hibernate settings,devprofile active - Temporarily exclude
SecurityAutoConfiguration(security is out of scope of this tutorial for now) - Pin minimum versions of Tomcat, Logback and Jackson to mitigate CVEs not patched in Spring Boot 2.7.x
-
CategoryandProductentities with Bean Validation (@NotBlank,@Size,@Positive,@ManyToOne) -
CategoryDto,ProductDtoand their MapStruct mappers - Repositories, service contracts and implementations (logging, transactions)
-
ResourceNotFoundExceptionandGlobalExceptionHandler(error/404,error/500) -
CategoryControllerandProductControllerwith the six routes above - Thymeleaf views (
index,add,edit) for both resources, the shared layout, the dashboard - Factories and seeders (
devprofile) - Service tests (Mockito) and controller tests (
MockMvc)
-
Customerentity (@NotBlank,@Email,@Size),CustomerDto, mapper - Repository, service contract and implementation
-
CustomerControllerand its views, with inline validation errors and flash messages -
CustomerFactoryandCustomerSeeder - Service tests (Mockito) and controller tests (
MockMvc)
-
Orderentity (@ManyToOneonCustomerandProduct),OrderDtocarryingcustomerId/productIdplus the display fieldscustomerName,productName,unitPrice -
OrderMapper, repository, service contract and implementation -
OrderControllerand its views, with product and customer dropdowns pre-filled on the edit page -
OrderFactoryandOrderSeeder(random customers and products) - Service tests (Mockito) and controller tests (
MockMvc), including the not-found case for an unknown order
feature/config→ Pull Request todevelopfeature/products(depends onconfig) → Pull Request todevelopfeature/customers(depends onproducts) → Pull Request todevelopfeature/orders(depends onproductsandcustomers) → Pull Request todevelopdevelop→master
- Root package:
com.edgareldy.spring_mvc_tutorial - Lombok on entities and DTOs; MapStruct for every entity ↔ DTO conversion
- A controller never touches a repository or an entity directly: only a service, and only DTOs
- A service is declared as an interface in
service/and implemented inservice/impl/ - Validation rules live on the entity and on the DTO, and are surfaced inline in the views
- Every mutating action redirects (Post/Redirect/Get) with a flash message
- Seeders only run under the
devprofile - Test methods follow the
_NN_Should<Outcome>_When<Condition>naming convention (see Test naming convention)
- Spring MVC controllers, view resolution and the Post/Redirect/Get pattern
- Server-side rendering with Thymeleaf and a shared layout (
thymeleaf-layout-dialect) - Spring Data JPA and Hibernate with MySQL
- The Entity / DTO / Mapper / Repository / Service layering, with MapStruct
- Bean Validation with inline error rendering
- Centralized error handling with
@ControllerAdvice - Seeding a development database with JavaFaker
- Service tests with Mockito and controller tests with
MockMvc
- Clone the repository and check out
develop - Follow the branches in order:
feature/config→feature/products→feature/customers→feature/orders - Create a MySQL database named
spring_dband adjust the credentials insrc/main/resources/application.properties - Run the tests with
./mvnw test - Run the application with
./mvnw spring-boot:run, then openhttp://localhost:8080/