Thank you for your interest in contributing to learn-dev, a learning platform to teach programming.
Whether you're fixing a bug, proposing a new feature, improving documentation, or writing tests, every contribution helps make this project better. This guide will walk you through the process of contributing.
If you have any questions, feel free to open an issue on GitHub.
This page is quite long, so use the hamburger menu
to navigate the table of contents more easily.
You can find it at the top right of the page (see below).
Make sure you read and agree to our Code of Conduct.
Read the Prerequisites section of the README.
The code reference (Javadoc) is
published on GitHub Pages,
rebuilt from dev on every merge. To read the reference for the branch you
are working on, build it locally with make javadoc and open
target/reports/apidocs/index.html (see
Generating the Documentation).
Significant architectural and design decisions are recorded as ADRs under
docs/adr/, as Markdown files using the MADR structure.
ADRs are an append-only, numbered log: a decision is never rewritten.
A new ADR supersedes an old one.
Files are named NNNN-short-title-in-kebab-case.md (4-digit zero-padded sequence).
When creating an ADR use docs/adr/template.md as a template,
then add a link to the new ADR to the index in docs/adr/README.md.
learn-dev is a Web-based client-server application using a PostgreSQL database to persist information.
graph TD
U["👤 User (Browser)"]
subgraph BE["Backend (Spring Boot / Java)"]
Security["Spring Security (session cookie, CSRF)"]
Controllers["Spring MVC Controllers (@Controller)"]
Services["Services"]
Repositories["Repositories (Spring Data JPA)"]
Views["Thymeleaf Views (server-rendered HTML)"]
end
DB[("PostgreSQL Database")]
MP["Mailpit (fake SMTP, dev only)"]
U -->|"HTTP GET / form POST"| Security
Security --> Controllers
Controllers --> Services
Services --> Repositories
Repositories -->|"SQL (Hibernate)"| DB
Services -->|"SMTP (password reset email)"| MP
Controllers -->|"view name + model"| Views
Views -->|"HTML page"| U
There is no separate frontend application and no REST API in v1: the backend renders HTML pages server-side with Thymeleaf and authentication uses a session cookie (no JWT).
sequenceDiagram
actor U as User (Browser)
participant SEC as Spring Security
participant UDS as CustomUserDetailsService
participant DB as PostgreSQL
U->>SEC: GET /auth/login
SEC-->>U: Login page (Thymeleaf form with CSRF token)
U->>SEC: POST /auth/login<br/>username + password + CSRF token
SEC->>UDS: loadUserByUsername(username)
UDS->>DB: SELECT user and roles WHERE username = ?
alt User not found or password mismatch (BCrypt check)
SEC-->>U: 302 redirect to /auth/login?error
end
SEC->>SEC: Create HTTP session
SEC-->>U: 302 redirect to /dashboard<br/>Set-Cookie: JSESSIONID (HttpOnly, SameSite=Lax)
U->>SEC: GET /dashboard (session cookie)
SEC-->>U: Dashboard page (server-rendered by Thymeleaf)
Spring Security handles the login POST itself
(UsernamePasswordAuthenticationFilter); no controller code is involved.
CustomUserDetailsService loads the user and their roles from PostgreSQL,
and the password is verified against its BCrypt hash.
Registration (POST /auth/register) is handled by AuthController and
RegistrationService (server-side bean validation plus duplicate
username/email detection).
sequenceDiagram
actor U as User (Browser)
participant C as PasswordResetController
participant S as PasswordResetService
participant DB as PostgreSQL
participant M as Mailpit (SMTP, dev)
U->>C: GET /auth/forgot-password
C-->>U: Email form (CSRF token)
U->>C: POST /auth/forgot-password (email)
C->>S: requestReset(email, ip, resetUrlBase)
alt Unknown email or rate limit exceeded
S->>DB: Record the attempt in audit_logs
else Known email, within the rate limit
S->>DB: Invalidate the outstanding tokens
S->>DB: Store the SHA-256 hash of a new random token
S->>M: Email the reset link (raw token in the URL)
S->>DB: Record the request in audit_logs
end
C-->>U: 302 redirect to ?sent (same answer either way)
U->>C: GET /auth/reset-password?token=...
C->>S: findUsableToken(raw)
S->>DB: SELECT by SHA-256(token), unused, not expired
C-->>U: New password form (or invalid link message)
U->>C: POST /auth/reset-password (token + new password)
C->>S: resetPassword(raw, newPassword, ip)
S->>DB: Store the BCrypt hash, consume the token,<br/>invalidate the others, audit
C-->>U: 302 redirect to /auth/login?reset
The flow is enumeration-safe: whether the email exists or not (or the
per-user / per-IP rate limit was exceeded), the browser gets the same
neutral confirmation, so an attacker cannot use the form to discover
accounts. Only the SHA-256 hash of the token is stored; the raw token
appears once, in the emailed link. A token is single-use, only one is
active per user at a time, and it expires after 30 minutes (tunable
under learndev.password-reset.* in application.yaml). Every request
and reset attempt is recorded in the audit_logs table by AuditService.
In development the email lands in Mailpit; the
end-to-end journey (request, email, link, new password) is covered by
PasswordResetFlowTest.
The state diagrams below (designed in issue
#42) describe how a
student progresses through a course, and how a course and a lesson move
between their editorial states. They drive the status columns of the
enrollments, courses, and lessons tables.
Not every state is persisted in v1:
- BrowsingCourses and ViewingCourseDetails are plain navigation, not stored state.
- EnrollmentPending / EnrollmentFailed (prerequisites) are future:
v1 has no prerequisites, so enrolling succeeds synchronously and
Enrolled / NotStarted collapse into the
ENROLLEDstatus. - CourseUpdated / LessonUpdated are transient editing views of
PUBLISHED, not a stored status. - CourseDeleted / LessonDeleted are future: v1 models removal as
ARCHIVED(nothing destructive), so the stored statuses areDRAFT,PUBLISHED, andARCHIVED.
The state diagram below shows the student lifecycle from enrollment to the end of the course.
stateDiagram-v2
[*] --> BrowsingCourses
BrowsingCourses --> ViewingCourseDetails: View course overview
ViewingCourseDetails --> EnrollmentPending: Click "Enroll" to sign up
EnrollmentPending --> Enrolled: Enrollment successful
EnrollmentPending --> EnrollmentFailed: Prerequisites missing
EnrollmentFailed --> ViewingCourseDetails: Try again / choose another course
Enrolled --> NotStarted: Course access granted
NotStarted --> InProgress: Start first lesson
InProgress --> InProgress: Complete lesson
InProgress --> Completed: All required lessons completed
InProgress --> Dropped: Drop/withdraw from course
NotStarted --> Dropped: Drop/withdraw before starting
Completed --> [*]
Dropped --> [*]
stateDiagram-v2
[*] --> CourseDraft
CourseDraft --> CoursePublished: Publish course
CourseDraft --> CourseArchived: Archive course
CoursePublished --> CourseDraft: Unpublish for editing (optional)
CoursePublished --> CourseUpdated: Update course
CoursePublished --> CourseArchived: Archive course
CourseUpdated --> CoursePublished: Re-publish course
CourseUpdated --> CourseArchived: Archive course
CourseArchived --> CourseDraft: Restore / re-open (optional)
CourseArchived --> CourseDeleted: Delete course
CourseDeleted --> [*]
stateDiagram-v2
[*] --> LessonDraft
LessonDraft --> LessonPublished: Publish lesson
LessonDraft --> LessonArchived: Archive lesson
LessonPublished --> LessonDraft: Unpublish for editing (optional)
LessonPublished --> LessonUpdated: Update lesson
LessonPublished --> LessonArchived: Archive lesson
LessonUpdated --> LessonPublished: Re-publish lesson
LessonUpdated --> LessonArchived: Archive lesson
LessonArchived --> LessonDraft: Restore / re-open (optional)
LessonArchived --> LessonDeleted: Delete lesson
LessonDeleted --> [*]
The frontend look and feel is specified before code:
- Figma file (read-only): low-fidelity wireframes (page structure) and high-fidelity mockups (Catppuccin theme).
- Browsable HTML mockups: the same pages as static HTML/CSS; their markup and BEM classes are the blueprint for the future Thymeleaf templates.
- docs/design/theme-exploration.md: the design tokens and their WCAG contrast ratios; mockups-explained.md (FR: mockups-explained-fr.md) and the per-file docs in docs/design/mockups/ explain every tag and CSS rule.
Frontend contributions are expected to follow the mockups, consume colors only through the design tokens, use the BEM naming convention (see Code Style and Formatting), and preserve the accessibility wiring documented in docs/rgaa.md.
We use a monorepo, that is a Git repository containing mainly both the frontend and the backend.
Using a monorepo offers the following advantages:
- Shared code and types:
The frontend and backend can share types from a single source of truth, avoiding duplication and keeping them in sync. - Atomic changes:
A single commit or PR can update the database schema, backend API, and frontend together, ensuring they stay compatible. - Easier code reviews:
Reviewers can see the full picture of a change (database + backend + frontend) in a single PR rather than coordinating across multiple repositories. - Consistent tooling and configuration:
Linting rules, formatting, CI/CD pipelines, and Git hooks are configured once and apply to all packages.
The Spring Boot application lives at the repository root (standard Maven layout):
learn-dev/
├── .env.example # Template for the local .env file (see README)
├── .github/
│ └── workflows/ # CI: one workflow per concern (build, test, lint, schema drift)
├── .sdkmanrc # Pins the Java and Maven versions (SDKMAN)
├── Makefile # Developer shortcuts (run, test, diagrams, ...)
├── config/
│ └── checkstyle/
│ └── checkstyle.xml # Project Checkstyle ruleset (advisory lint)
├── docker-compose.yaml # PostgreSQL, MongoDB, and Mailpit services
├── mvnw # Maven wrapper
├── pom.xml # Dependencies, build configuration, and metadata
│
├── docker/
│ └── init/
│ └── 01-create-app-db-user.sh # Runs once on first postgres container start to create the application database and user
│
├── docs/
│ ├── adr/ # Architecture Decision Records (MADR)
│ ├── database/merise/ # MCD, MLD, MPD diagrams and sources
│ ├── design/ # Design tokens study, HTML mockups, Figma links
│ ├── plans/ # Implementation plans
│ ├── rgaa.md # Accessibility (RGAA) criteria map
│ └── tech-stacks.md # Catalogue of tools and frameworks
│
└── src/
├── main/
│ ├── java/com/ericbouchut/learndev/
│ │ ├── LearnDevApplication.java # Spring Boot entry point
│ │ │
│ │ ├── admin/ # Administration area (/admin/**, ROLE_ADMIN)
│ │ │ ├── AdminController.java # Account list/creation, content moderation
│ │ │ └── AccountAdminService.java # Instructor creation, archive/reactivate, guards
│ │ │
│ │ ├── audit/ # Security audit trail (audit_logs table)
│ │ │ ├── AuditService.java # Records auditable events (reset requests, ...)
│ │ │ ├── entity/
│ │ │ │ └── AuditLog.java
│ │ │ └── repository/
│ │ │ └── AuditLogRepository.java
│ │ │
│ │ ├── auth/ # Authentication (registration, login, password reset)
│ │ │ ├── AuthController.java # Web pages: home, login, register
│ │ │ ├── CustomUserDetailsService.java # Loads user + roles from DB for Spring Security
│ │ │ ├── PasswordResetController.java # Forgot password and reset password pages
│ │ │ ├── PasswordResetMailer.java # Sends the reset link over SMTP
│ │ │ ├── PasswordResetService.java # Token issue/consume, rate limit, enumeration safety
│ │ │ ├── RegistrationService.java # Creates accounts (hashing, default role, duplicates)
│ │ │ ├── dto/
│ │ │ │ ├── ForgotPasswordForm.java
│ │ │ │ ├── RegisterForm.java # Form backing beans (bean validation)
│ │ │ │ └── ResetPasswordForm.java
│ │ │ ├── entity/
│ │ │ │ └── PasswordResetToken.java # Maps to the reset_tokens table
│ │ │ ├── exception/
│ │ │ │ ├── DuplicateEmailException.java
│ │ │ │ └── DuplicateUsernameException.java
│ │ │ └── repository/
│ │ │ └── PasswordResetTokenRepository.java
│ │ │
│ │ ├── common/ # Concerns shared across features
│ │ │ └── config/
│ │ │ ├── CacheConfig.java # Caffeine cache (rendered lesson Markdown)
│ │ │ └── SecurityConfig.java # Spring Security filter chain, role gates, PasswordEncoder
│ │ │
│ │ ├── course/ # Course domain and its web layers
│ │ │ ├── CourseController.java # Student catalogue, detail, lessons, enroll/drop
│ │ │ ├── CourseService.java # Student visibility rules (read side)
│ │ │ ├── DashboardController.java # The student's courses (GET /dashboard)
│ │ │ ├── EnrollmentService.java # Enroll/drop lifecycle (idempotent)
│ │ │ ├── InstructorCourseController.java # Authoring area (/instructor/**, ROLE_INSTRUCTOR)
│ │ │ ├── InstructorCourseService.java # Ownership, publish/archive/restore, reorder, roster
│ │ │ ├── MarkdownRenderer.java # Lesson Markdown to sanitized HTML (ADR-0013, ADR-0014)
│ │ │ ├── dto/
│ │ │ │ ├── CourseForm.java
│ │ │ │ └── LessonForm.java
│ │ │ ├── entity/
│ │ │ │ ├── Course.java # Maps to the courses table
│ │ │ │ ├── Enrollment.java # Join entity on the enrollments table
│ │ │ │ ├── EnrollmentId.java # (user, course) composite key
│ │ │ │ ├── EnrollmentStatus.java # ENROLLED, IN_PROGRESS, COMPLETED, DROPPED
│ │ │ │ ├── Lesson.java # Maps to the lessons table
│ │ │ │ └── PublicationStatus.java # DRAFT, PUBLISHED, ARCHIVED
│ │ │ └── repository/
│ │ │ ├── CourseRepository.java
│ │ │ ├── EnrollmentRepository.java
│ │ │ └── LessonRepository.java
│ │ │
│ │ ├── legal/ # Legal pages
│ │ │ └── LegalController.java # Privacy policy page (French)
│ │ │
│ │ ├── role/ # Role management
│ │ │ ├── entity/
│ │ │ │ └── Role.java # Maps to the roles table
│ │ │ └── repository/
│ │ │ └── RoleRepository.java
│ │ │
│ │ └── user/ # User account management
│ │ ├── entity/
│ │ │ └── User.java # Maps to the users table
│ │ └── repository/
│ │ └── UserRepository.java
│ │
│ └── resources/
│ ├── application.yaml # Main config (datasource, Liquibase, mail, session cookie)
│ ├── application-dev.yaml # Dev profile overrides
│ ├── application-prod.yaml # Prod profile overrides
│ ├── db/
│ │ └── changelog/ # Liquibase migrations
│ │ ├── db.changelog-master.yaml # Master changelog (includeAll on changes/)
│ │ └── changes/
│ │ ├── V20260608161836-create-users-table.sql # One changeset per file
│ │ └── ... # Tables, indexes, seeds (applied in filename order)
│ ├── static/
│ │ ├── css/ # Design system: base.css, theme and font stylesheets
│ │ └── fonts/ # Self-hosted webfonts (OFL license files alongside)
│ └── templates/ # Thymeleaf views (server-rendered HTML)
│ ├── admin/ # Administration pages (accounts, moderation)
│ │ ├── courses.html
│ │ ├── user-form.html
│ │ └── users.html
│ ├── courses/ # Student course pages
│ │ ├── catalog.html
│ │ ├── detail.html
│ │ └── lesson.html
│ ├── error/ # Styled error pages (403, 404, 500)
│ ├── fragments/
│ │ └── layout.html # Shared head, header (nav), and footer fragments
│ ├── instructor/ # Authoring pages
│ │ ├── course-form.html
│ │ ├── courses.html
│ │ ├── lesson-form.html
│ │ └── students.html
│ ├── dashboard.html
│ ├── forgot-password.html
│ ├── home.html
│ ├── login.html
│ ├── privacy.html # Privacy policy (French)
│ ├── register.html
│ └── reset-password.html
│
└── test/
└── java/com/ericbouchut/learndev/
├── LearnDevApplicationTests.java # Context load smoke test
│
├── auth/
│ ├── AuthFlowTest.java # End-to-end register, login, dashboard flow (MockMvc + Testcontainers)
│ ├── CustomUserDetailsServiceTest.java
│ ├── PasswordResetFlowTest.java # End-to-end password reset through a real Mailpit container
│ └── RegistrationServiceTest.java
│
├── legal/
│ └── PrivacyPageTest.java # The public privacy page renders
│
├── role/repository/
│ └── RoleRepositoryTest.java # @DataJpaTest with Testcontainers
│
├── support/
│ └── AbstractPostgresIT.java # Shared Testcontainers PostgreSQL base class
│
└── user/repository/
└── UserRepositoryTest.java # @DataJpaTest with TestcontainersThe table below explains what each folder entails.
| Folder | Purpose |
|---|---|
src/main/java/.../learndev/ |
Spring Boot application root: entry point and top-level package |
src/main/java/.../learndev/audit/ |
Security audit trail: records auditable events in audit_logs |
src/main/java/.../learndev/auth/ |
Authentication: registration, login support, password reset, auth pages |
src/main/java/.../learndev/common/ |
Cross-cutting concerns: Spring Security configuration |
src/main/java/.../learndev/legal/ |
Legal pages (privacy policy) |
src/main/java/.../learndev/role/ |
Role entity and data access |
src/main/java/.../learndev/user/ |
User entity and data access |
src/main/resources/static/ |
Design system stylesheets and self-hosted webfonts |
src/main/resources/templates/ |
Thymeleaf views rendered server-side |
src/main/resources/db/changelog/ |
Liquibase database migrations |
src/test/java/.../learndev/ |
Tests (unit and Testcontainers-backed integration tests, all *Test) |
The project uses a feature-based layout for packages, as you can see in the directory structure above,
The main advantages in my opinion are:
- Navigability:
When you open one package (such asuser), you see all the classes related to the user feature. - Encapsulation
In layer-based, every class must bepublicbecause thecontrollerpackage needs to reach theservicepackage, which needs therepositorypackage.
In feature-based, the controller, service, and repository are in the same package, so you can use package-private visibility to hide internals. Only the few classes that genuinely cross feature boundaries need to bepublic.
Note
Each backend feature package (e.g. auth, user, role...) follows the same layout.
A feature package contains the controller and service classes,
and the following sub-packages entity,repository, dto, and exception.
- Folder: snake_case
- Files
- Backend
- PascalCase: Classes use PascalCase and are suffixed by their
layer role:
CourseController,CourseService,Course,CourseRepository,CourseResponse:- Configuration classes end with
Config. - Exception classes end with
Exception. - Entity classes have no suffix (they represent the domain noun directly) (e.g.,
Course). An entity represents a database table as a Java object. It defines what the data looks like: which fields exist, their types, their constraints.
For example,Course.javamaps to yourcoursestable and contains fields likename,description. - Repository classes define how you access data such as finding a course by name,
saving a new course, deleting a course. For example,
CourseRepository.javamay provide methods such asfindByName(String name)orsave(Course course). - Service classes contains business logic.
- DTO data transfer classes carry data between layers or across the API boundary. They decouple the API's input/output shapes from the database entities and prevent sensitive fields (e.g. password hash) from leaking into responses.
- Controller: Spring
@RestControllers that maps HTTP requests to handler methods. They delegate business logic to the service layer and returns the appropriate HTTP response and status code. - ...
- Configuration classes end with
- PascalCase: Classes use PascalCase and are suffixed by their
layer role:
- Backend
Note
What are PascalCase, snake_case, and camelCase?
- PascalCase is a naming convention where the first letter of every word
is capitalized, with no spaces or underscores between words,
e.g.:LearnDevApplication - snake_case
is a naming convention where words are lowercase and separated
with underscores (
_),
e.g.:learn_dev_application - camelCase is a naming convention
where the first word starts with a lowercase letter and each subsequent
word begins with an uppercase letter, with no spaces or underscores,
e.g.:learnDevApplication
- Authentication endpoints are grouped under the
/auth/prefix:/auth/login,/auth/register,/auth/logout(and future/auth/reset-password). This centralizes everything related to authentication and mirrors the feature-based package layout (theauthpackage owns/auth/**). - Application pages stay at the root or under their own feature prefix
(for example
/dashboard,/courses/**), not under/auth/, since they are not authentication actions.
The learn-dev platform uses a PostgreSQL relational database to persist entities.
We use Liquibase to manage the database schema.
Migrations live in src/main/resources/db/changelog/.
Liquibase uses:
-
src/main/resources/db/changelog/db.changelog-master.yaml:
a configuration file that defines the format and the order of migrations to apply. It lists every change file viaincludeAllon thechanges/directory (applied in filename order). -
changes/VYYYYMMDDHHMMSS-short-description.sql
a migration file (called change file) to apply.
Each change file contains a pair of directives (specially crafted SQL comments): the changeset to apply to the database structure and how to roll it back. It contains DDL (Data Definition Language) SQL. and are named with a timestamp prefix: -
The Change files have a naming convention of:
VYYYYMMDDHHMMSS-short-description.sqle.g.
V20260608161842-create-idx-user-roles-role-id.sql.
V + UTC timestamp keeps files ordered chronologically
and avoids numbering collisions when branches add migrations in parallel.
Example: src/main/resources/db/changelog/changes/V20260608161836-create-users-table.sql
--liquibase formatted sql
-- users: application accounts. UUID PK — non-enumerable and future-API-safe (ADR-0003).
--changeset ebouchut:V20260608161836
CREATE TABLE users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, -- bcrypt/argon2 hash, never plaintext
first_name VARCHAR(100),
last_name VARCHAR(100),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
is_locked BOOLEAN NOT NULL DEFAULT FALSE,
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
last_login_at TIMESTAMPTZ,
password_changed_at TIMESTAMPTZ
);
--rollback DROP TABLE users;Where:
--liquibase formatted sql
This special SQL comment indicates that this file is a Liquibase migration in SQL format.--changeset ebouchut:V20260608161836
This comment should be located before the SQL changeset to update the database structure (here add theuserstable). It contains :author:id.--rollback DROP TABLE users;
this SQL statement to rollback the change should be prefixed with--rollback(on the same line).
Important
- Each file should contain a single changeset per file prefixed with
--changeset author:id - The changeset id equals the filename's timestamp, e.g.
>
--changeset ebouchut:V20260608161842. This keeps the id globally unique. - Migrations are append-only: never edit a changeset that has already run on a shared database — add a new one.
- Each changeset should have a
--rollbacksection. - After adding or removing a column, update the MCD diagram source
(
docs/database/merise/learn-dev.mcd) to match, then runmake check-schema-drift && make diagramscheck-schema-driftverifies every table column is represented in the diagram. CI runs this check too.
Here are the naming conventions for the name of our database tables:
- All lowercase
- Plural
- Use underscore for multi words names:
social_networks - Less than 64 characters (remnant of a MySQL constraint, just in case ;-)
Do not use an underscore as the first character.
This section describes the data model using the progressive 3 diagrams (MCD, MLD, and MPD) from the Merise methodology. This gives a view from high-level conceptual model (MCD), logical model (MLD) to physical model (MPD) with all the database details.
MCD stands for 🇫🇷 Modèle Conceptuel de Données (Conceptual Data Model). The MCD diagram is part of the Merise methodology and shows the entities and relationships without the (database) technical details.
It is a high-level business-domain oriented diagram
that shows the data (entities), their relationships and cardinalities,
with NO technical and implementation details.
MLD stands for 🇫🇷 Modèle Logique de Données (Logical Data Model). The MLD diagram is part of the Merise methodology and shows the Logical Data Model.
It shows the relational structure in a database-agnostic way. It is a transformed version of the MCD where:
- entities become tables,
1..Nrelationships become foreign keys,N..Nrelationships become junction tables,1..1relationships become foreign keys.
The MLD is shared with domain experts and application developers.
Domain experts can verify that the relational structure accurately reflects
the business.
Application developers can then start creating the entities.
Note
Regenerating the MLD: the single source of truth is the conceptual MCD
(learn-dev.mcd). Run make mld: it auto-derives the logical model
(learn-dev_mld.mcd, via mocodo's -t diagram), emits the relational schema
as Markdown (learn-dev_mld.md, via -t mld), and renders the diagram
(learn-dev_mld.svg). The learn-dev_mld.* files are generated artifacts —
do not edit them by hand; edit only learn-dev.mcd.
MPD stands for 🇫🇷Modèle Physique des Données (Physical Data Model).
This diagram is exhaustive and database-specific.
It contains all the tables, fields, keys, database-specific data types,
and constraints.
It is aimed at database administrators and application developers:
- Developers use it to implement the data model in the app.
- Database administrators use it to create the migration scripts.
The full MPD documentation (in Markdown)
contains what is missing from the above diagram due to space limitations.
This is why the SVG and the full MPD documentation) work together.
Use the SVG to navigate and the Markdown to look up specifics:
- The SVG above gives a visual overview of every table, column, and foreign key relationship at a glance.
- The Markdown is the exhaustive reference (with all constraints).
For each table,docs/database/merise/mpd/public.<table>.mdlists every column's type, default, and nullability, plus the full constraints (primary keys, foreign keys, and uniqueness) and indexes — details that a diagram cannot convey.
Note
The MPD is generated by tbls from the live
PostgreSQL database (not from the MCD), so it always reflects the real schema.
Run make mpd to regenerate it.
erDiagram
users ||--o{ user_roles : "has"
roles ||--o{ user_roles : "granted via"
users ||--o{ email_tokens : "has"
users ||--o{ reset_tokens : "has"
users ||--o{ audit_logs : "generates"
users {
uuid user_id PK
varchar username
varchar email
varchar password
varchar first_name
varchar last_name
boolean is_active
boolean is_verified
boolean is_locked
integer failed_login_attempts
timestamptz last_login_at
timestamptz password_changed_at
}
roles {
bigint role_id PK
varchar role_name
varchar description
boolean is_active
}
user_roles {
uuid user_id PK,FK
bigint role_id PK,FK
timestamptz assigned_at
uuid assigned_by FK
}
email_tokens {
bigint token_id PK
varchar token
timestamptz expires_at
timestamptz used_at
uuid user_id FK
}
reset_tokens {
bigint token_id PK
varchar token
timestamptz expires_at
timestamptz used_at
inet ip_address
uuid user_id FK
}
audit_logs {
bigint log_id PK
varchar action_type
varchar entity_type
varchar entity_id
text description
inet ip_address
text user_agent
boolean was_successful
text error_message
jsonb metadata
timestamptz created_at
uuid user_id FK
}
Tip
If you want to create an Entity Relationship Diagram (ERD) like this one, then take a look at Mermaid.js. With this syntax embedded in a Markdown file, GitHub issue, you can easily create many types of diagrams such as sequence/flow/class/state diagrams to only name a few.
GitHub among many other tools, IDEs and platforms support Mermaid diagrams.
To give Mermaid diagrams a whirl, you can use the free online visual editor to build your first diagram, share it with others and even export it to various formats.
Note
This diagram uses Crow's foot notation for the cardinality of relationships, where:
o|denotes0..1(zero or one)||denotes Exactly oneo{denotes0..n(zero or more)
If you encounter a bug, please open an issue and include:
- A clear and descriptive title.
- Steps to reproduce the problem (code snippets, commands, or screenshots if relevant).
- What you expected to happen.
- What actually happened (including any error messages).
- Your environment details (OS, browser, Node/Python/runtime versions, etc. if applicable).
This information helps maintainers reproduce and fix the issue more quickly.
We welcome ideas to improve learn-dev (new features, UX improvements, performance, documentation, etc.). Before opening a feature request, please:
- Check the existing issues to see if a similar idea already exists.
- Add a comment to an existing issue if it matches your idea, rather than opening a duplicate.
If you open a new enhancement issue, try to explain the problem you are trying to solve, not only the solution you have in mind.
When creating a feature request issue, you can use the following structure:
- Summary: A short description of the feature.
- Problem: What problem does this feature solve for users?
- Proposed solution: How you think this feature could work (UI/API/behavior).
- Alternatives: Any alternative solutions or workarounds you have considered.
- Additional context: Screenshots, diagrams, or links that help explain the idea.
If you are new to the project, we recommend starting with issues labeled
good first issue or
help wanted on GitHub.
Steps to get started:
- Pick an open issue with one of these labels.
- Comment on the issue to indicate that you would like to work on it.
- Wait for a maintainer to confirm or provide additional guidance before investing a lot of time.
If you are unsure where to start, you can also open an issue asking for pointers or clarification.
At a high level, you will usually need to:
- Fork the repository and clone your fork locally.
- Follow the setup instructions in the project’s README files (for backend, frontend, or other components).
- Install the required dependencies using the package manager(s) mentioned there.
- Run the test suite or basic checks to ensure everything works before you start making changes.
For more detailed, component-specific instructions, please refer to the corresponding README files in each subdirectory.
Note
This project uses a very lightweight version of Git Flow (without release branch) branching workflow to develop, integrate, release new features, and fix bugs.
Why did we make this change?
Usually, main is the default branch and serves both for the "integration" and deployment to production,
which makes these processes brittle.
What is this change about?
To facilitate the integration of several features, we created the dev branch.
This is where we share the common code with the team.
It also serves as an integration branch to ensure that all the merged-in features work properly.
This new branching scheme makes dev the new default branch.
We use a monorepo, meaning it contains both the frontend and the backend.
gitGraph
commit
branch dev
checkout dev
branch feat/add-home-page
checkout feat/add-home-page
commit
commit
checkout dev
merge feat/add-home-page
branch feat/add-footer
commit
commit
checkout dev
merge feat/add-footer
checkout main
merge dev
commit type: HIGHLIGHT tag: "bug found"
checkout main
branch fix/htaccess
commit
checkout main
merge fix/htaccess
checkout dev
merge fix/htaccess
The branches:
devis the main development branch.- The repository uses this branch as the primary one for development and Pull Requests.
- It acts as an "integration" branch contains the common code and serves as a safety net.
- This branch contains the code shared with the team.
- This is the default branch, meaning it is the one we are on after cloning this repository, the base branch of our PRs.
- It is also where we test that the merged features do not break the website.
Once we are confident the code ondevcan be deployed to production, we mergedevintomain. - We should not commit directly to
dev, but create a PR (Pull Request) to bring in changes. - As this is a solo project for now, no review approval is required to merge a PR.
Once the team grows, protect the
devbranch with a rule requiring at least two approving reviews before merging.
maincontains the production-ready code.- This is where the team merges
devafter ensuring that the new features ondevare working properly together. - This branch is used for deployment.
- This is where the team merges
You create temporary branches to develop a feature or fix a bug:
feat/add-home-pagedenotes a feature branch.
The naming convention may seem a bit convoluted, but refer to:- the kind of branch it is:
feat(this is a feature), - what the branch will bring when merged to
devsuch asadd-home-page, a concise description of the feature, using all-lowercase words separated with an hyphen.
- the kind of branch it is:
fix/htaccess(orfix/broken-link-page-footer) are bug fix branches created when the website breaks in production.
They are branched off frommain, then merged intomainto fix the bug and thendevto backport the fix to the main development branch.
Note
dev is the base branch of PRs.
This is where GitHub merges the head branch, such as a
feature branch (feat/add-home-page) or a bugfix branch (fix/broken-link-page-footer).
---
title: GitHub PR branches
config:
gitGraph:
showCommitLabel: false
mainBranchName: "dev (base branch)"
---
gitGraph
commit
commit
branch "feat/add-home-page (head branch)"
checkout "feat/add-home-page (head branch)"
commit
commit
checkout "dev (base branch)"
merge "feat/add-home-page (head branch)" type: HIGHLIGHT
This project follows the Conventional Commits specification.
Format: <type>(<scope>): <description>
-
Type— Nature of the change (required):Type When to use featA new feature fixA bug fix docsDocumentation-only changes styleFormatting or whitespace — no logic change refactorCode change that neither adds a feature nor fixes a bug testAdding or correcting tests choreBuild process, tooling, or dependency updates -
Scope— Optional area of the codebase in parentheses (e.g.,auth,README,git). -
Description— Short imperative-mood summary, capitalized, no trailing period.
Examples:
feat(auth): Add JWT refresh token rotation
fix(user): Return 404 when user is not found
docs(README): Add prerequisites section
test(course): Cover edge case for empty course list
chore(git): Ignore IntelliJ IDEA configuration files
- Java: standard 4-space indentation. The style is checked by
Checkstyle against the project ruleset
config/checkstyle/checkstyle.xml, a copy of the Google ruleset adapted to this project's conventions (4-space indentation, IDE-managed import order; the adaptations are documented in the file header). Checks are advisory for now: see ADR-0012, which restates the advisory-lint stance of ADR-0011. - HTML/CSS: 2-space indentation (the convention used by the Thymeleaf
templates in
src/main/resources/templates/).
- Locally: run the check and open the browsable report:
Violations also print on the console as warnings, and the raw XML result is written to
./mvnw -B checkstyle:checkstyle open target/reports/checkstyle.html # macOStarget/checkstyle-result.xml. - Online: the latest report from
devis rendered on GitHub Pages (republished by the Lint workflow on each merge todev, without any commit). - On CI: every Lint workflow run
uploads both as the
checkstyle-reportartifact. Open a run, scroll to its Artifacts section, download the archive, and openreports/checkstyle.htmlinside it.
This section explains how to reset the development database. It may prove useful when you need to start from a blank slate.
TODO: Add how to reset the development database
The command above:
- Drops then recreates the database schema that is the structure (tables...),
- Applies all database migrations to recreate the database changes in chronological order,
- Runs a seed file to populate the database with development data.
Once you have picked up the latest changes from the upstream dev branch,
you need to apply the latest database migrations as follows:
TODO: How to apply the latest database migrations in development
To add, remove, an entity or a field/property you need to update the database schema. It serves as a source of truth used to generate and update the database.
Here is the workflow to add/update/remove the database structure (table, table column, relationship, enum value).
TODO: Explain how and where to update the database schema
We use Maven as a packages/dependencies manager on the backend.
-
Search for the artifact on Maven Central.
-
Copy the
<dependency>snippet (select the Maven tab). -
Paste it inside the
<dependencies>block ofpom.xml:<dependency> <groupId>com.example</groupId> <artifactId>some-library</artifactId> <version>1.2.3</version> </dependency>
Omit
<version>when the dependency is already managed by the Spring Boot parent POM (e.g.,spring-boot-starter-*,jackson-databind). -
Save
pom.xml. Maven downloads the artifact automatically on the next build or IDE sync. -
Verify the dependency resolves correctly:
mvn dependency:resolve
TODO: Explain how to write tests, what naming convention and best practices
- The file name of a test class should end in
Test. Although this is counterintuitive and the opposite of the standard Java method naming convention, it makes the test output easier to read. - Every test class ends in
Test(neverIT), including Testcontainers-backed integration tests, so Surefire runs the whole suite withmvn test. See ADR-0009 for the rationale; theITsuffix is reserved for non-test support classes such asAbstractPostgresIT.
Repository and integration tests run against a real PostgreSQL started by
Testcontainers
(see ADR-0006),
so a container engine must be running. This project uses Podman.
PasswordResetFlowTest also starts a Mailpit container the same way, to
receive the password reset email: no locally running Mailpit is needed to run
the tests.
The simplest way is to use make test, which configures Testcontainers for
Podman automatically:
make testIt is equivalent to ./mvnw test plus the Podman wiring described below.
Testcontainers looks for a Docker socket at /var/run/docker.sock,
which does not exist under Podman.
The workaround is to define two environment variables:
# Point Testcontainers at the Podman socket (resolved dynamically):
export DOCKER_HOST="unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')"
# Ryuk (the Testcontainers reaper) misbehaves under rootless Podman, so disable it:
export TESTCONTAINERS_RYUK_DISABLED=trueAdd these to your shell profile (for example ~/.zshrc),
source it, then run ./mvnw test directly, or just use make test,
which sets them for you.
Make sure the Podman machine is started first: podman machine start.
On real Docker (for example in CI) neither variable is needed; make test
falls back to a plain ./mvnw test.
Every test run measures code coverage with JaCoCo (see ADR-0011: coverage is reported, not yet enforced as a threshold).
- Locally:
make test(or./mvnw test) writes the report totarget/site/jacoco/index.html. Open it in a browser:open target/site/jacoco/index.html # macOS - On CI: every Tests workflow run
uploads the report as the
jacoco-coverage-reportartifact. Open a run, scroll to its Artifacts section, download the archive, and openindex.htmlinside it. - On Codecov: CI also uploads the report to the
Codecov dashboard
(see ADR-0012),
which powers the README coverage badge and comments on every PR with the
project and patch coverage. The Codecov statuses are informational
(configured in
codecov.yml): they report, they never block a merge.
Most of the documentation is hand-written and lives in the repository as Markdown: README, ARCHITECTURE, the ADRs, the glossaries, the accessibility docs, and the plans and design notes under docs/. Those you simply edit.
The rest is generated from the code or from the database. This section is about those:
| Documentation | Generate with | Output |
|---|---|---|
| API reference (Javadoc) | make javadoc |
target/reports/apidocs/index.html |
| Database diagrams (MCD, MLD, MPD) | make diagrams |
docs/database/merise/ |
| Code quality report (Checkstyle) | ./mvnw -B checkstyle:checkstyle |
target/reports/checkstyle.html |
| Test coverage report (JaCoCo) | make test |
target/site/jacoco/index.html |
The Checkstyle and JaCoCo reports have their own sections above (Checkstyle Report, Test Coverage Report (JaCoCo)). The two below do not.
The API reference is the Java documentation rendered from the /** */
comments in src/main/java. It holds one page per public or protected
type of com.ericbouchut.learndev (classes, interfaces, enums, records) with
its methods, their parameters and return values, plus package summaries, the
class hierarchy, and a searchable index.
- Locally: generate it, then open it:
This runs
make javadoc open target/reports/apidocs/index.html # macOS./mvnw javadoc:javadoc. The plugin is bound to no build phase, so an ordinarymake testor./mvnw packagenever pays the cost of building it. - Online: the reference built from
devis published on GitHub Pages (republished by the Lint workflow on each merge todev, without any commit). - On CI: every Lint workflow run
uploads it as the
javadocartifact. Open a run, scroll to its Artifacts section, download the archive, and openindex.htmlinside it.
Two things are worth knowing when reading the output:
- The Lombok accessors are absent. Javadoc reads the source, and the
getters and setters of the entities are generated at compile time by Lombok
(
@Getter/@Setter), so they never reach it. The fields they expose are documented; the accessors are not. - Javadoc does not police missing documentation. It runs with
doclint=all,-missing: it fails on a malformed comment, broken HTML, or a dangling{@link}, but says nothing about an absent@paramor@return. Reporting missing Javadoc is Checkstyle's job (MissingJavadocType,MissingJavadocMethod), and per ADR-0011 it reports rather than blocks.
The three Merise models are generated: the MCD and the MLD from the
Mocodo source docs/database/merise/learn-dev.mcd, and the MPD from the
live database with tbls. What they write
under docs/database/merise/ is committed to the repository but remains a
build artifact: edit the .mcd source, never the generated .svg/.md.
Prerequisites (see the
Python Setup section of the README): a Python
virtual environment with mocodo >= 4.3.3, and tbls on the PATH. The MPD
additionally needs the database running (docker compose up -d), because
tbls reads the real schema; its connection string is built from .env.
make mcd # MCD → learn-dev.svg
make mld # MLD → learn-dev_mld.svg
make mpd # MPD → mpd/ (one page per table, read from the live database)
make diagrams # all three
make clean # remove the generated MCD/MLD filesAfter a schema change, regenerate the diagrams and check that the model and the migrations still agree:
make check-schema-drift # fails if a Liquibase column is missing from the MCDCI runs that same check on every change to a migration or to the MCD (Schema drift workflow), so a diagram that has drifted from the schema blocks the merge.