How learn-dev is structured and how a request flows through it. This file answers "how do the pieces fit together, and why is it built this way?" For the list of tools and versions see docs/tech-stacks.md; for term definitions see GLOSSARY.md; for individual decisions and their trade-offs see the ADRs.
learn-dev is a server-rendered, layered Spring Boot monolith. The browser talks to Spring MVC controllers; controllers delegate to services; services use Spring Data JPA repositories over a PostgreSQL relational core. HTML is produced server-side by Thymeleaf. The app is a monolith today, with a longer-term intent to split selected concerns into microservices (which is why service-to-service auth is already being considered in ADR-0002).
-
Server-rendered MVC. Controllers return logical view names, not HTML or JSON; a Thymeleaf
ViewResolverrenders the corresponding template. -
Layered. Each layer depends only on the one below it:
Browser │ HTTP (form posts, GETs) ▼ Controller (web layer: @Controller, request mapping, validation) │ calls ▼ Service (business logic, @Transactional boundaries) │ calls ▼ Repository (Spring Data JPA interfaces) │ SQL via Hibernate ▼ PostgreSQL (relational core)
Packages are organised by feature, not by technical layer, so a feature's controller, service, entity, and repository live together:
com.ericbouchut.learndev
├── auth # AuthController, RegistrationService, CustomUserDetailsService,
│ # dto/RegisterForm, exception/Duplicate*Exception
├── user # entity/User, repository/UserRepository
├── role # entity/Role, repository/RoleRepository
├── common
│ └── config # SecurityConfig (filter chain, PasswordEncoder)
└── (test) support # AbstractPostgresIT (shared Testcontainers base)
- A controller method (for example
AuthController.home()) returns a view name such as"home"(a lookup key, not HTML). - The Thymeleaf
ViewResolvermaps the name tosrc/main/resources/templates/home.html. - The template engine renders the HTML, evaluating
th:*attributes, escaping output (XSS defence), and injecting the CSRF token into forms that useth:action. - The
DispatcherServletwrites the HTML as the response body with the appropriate headers and status.
A method may instead return a redirect: prefix (for example
redirect:/auth/login?registered), which produces a 302 rather than rendering a view.
- Session-based form login. Credentials are verified once; the session is kept
server-side and referenced by the
JSESSIONIDcookie (see ADR-0001). JWT was rejected for the browser flow. - Filter chain.
SecurityConfigdefines theSecurityFilterChain: public paths (/,/auth/**, static assets) are permitted; everything else requires authentication. Auth endpoints are grouped under the/auth/URL prefix. - User loading.
CustomUserDetailsServiceloads aUserby username and maps eachRoleto a Spring Security authority prefixed withROLE_(soADMINbecomesROLE_ADMIN). Account flags map todisabled(is_active) andaccountLocked(is_locked). - Passwords. Hashed with BCrypt; the raw password is never persisted.
- CSRF. Enabled by default; Thymeleaf injects a per-form token.
SameSite=Laxon the session cookie adds browser-level defence in depth. - Session cookie hardening.
HttpOnlyandSameSite=Laxare set in the base config;Secureis enabled once served over HTTPS.
RegisterForm (validated with Bean Validation) → AuthController → RegistrationService
(checks unique username/email, hashes the password, assigns the default STUDENT
role, saves) → redirect to the login page. Duplicate username/email surface as
field errors on the re-rendered form.
- Relational core (PostgreSQL). Users, roles, and (upcoming) courses/lessons.
Users use a UUID primary key to avoid enumeration; other tables use
BIGINTidentity (see ADR-0003). - Document store (MongoDB). Provisioned and configured for future content storage; not yet used by any feature.
- Schema evolution. Managed by Liquibase, run at startup. Migrations are hand-written formatted-SQL files, one atomic changeset per file, append-only (see ADR-0005).
- Modelling. The schema is designed in Merise (MCD → MLD → MPD) and cross-checked against the migrations by a schema-drift CI job.
- Profiles.
application.yamlholds base config;application-dev.yamlholds dev overrides.SPRING_PROFILES_ACTIVE=devselects the profile and the Liquibasedevcontext. - Secrets. Loaded from
./.env(a 1Password-filled FIFO) via spring-dotenv at startup, so the working directory must be the project root.
Tests form a pyramid, all run under Surefire in mvn test
(see ADR-0009):
- Unit tests — mock collaborators (Mockito), no I/O
(
RegistrationServiceTest,CustomUserDetailsServiceTest). - Slice tests —
@DataJpaTestagainst a real Postgres container (UserRepositoryTest,RoleRepositoryTest). - Integration test — full context + MockMvc for the end-to-end auth journey
(
AuthFlowTest). - Smoke test — verifies the context starts (
LearnDevApplicationTests).
Tests use a real PostgreSQL via Testcontainers rather than H2 (see ADR-0006), shared as a static singleton container (see ADR-0008).
make test— run the suite (Podman-aware Testcontainers wiring).make run— start the databases and run the app (http://localhost:8080/).docker compose up -d— start Postgres and Mongo (dockeris Podman here).