Sample Module¶
Reference implementation of a Spring Boot MVC module. Use this as a template when creating new modules (e.g. customer, platform, discussion-alignment).
Directory Structure¶
sample/
├── config/ Spring @Configuration beans
├── constants/ Static final values (API paths, Kafka topics, cache names)
├── controllers/ REST controllers and DTOs
│ └── dto/
│ ├── request/ Inbound request bodies (records)
│ └── response/ Outbound response bodies (records)
├── entities/ JPA entities and related enums
├── events/ Kafka messaging
│ ├── listeners/ @KafkaListener consumers
│ ├── messages/ Event payload records
│ └── producers/ KafkaTemplate publishers
├── integrations/ External HTTP clients (RestClient)
│ └── dto/
│ └── response/ DTOs for external API shapes
├── mappers/ Entity ↔ DTO conversion (@Component)
├── repositories/ Spring Data JPA interfaces
├── services/ Business logic (@Service)
└── utils/ Stateless helper methods
Patterns Demonstrated¶
Controller → Service → Repository¶
Standard MVC layering. Controllers handle HTTP concerns, services hold business logic, repositories handle persistence.
TodoController → TodoService → TodoRepository
Mapper Pattern¶
A @Component mapper centralizes all conversion logic between entities, DTOs, and events. This keeps DTOs as pure records with no mapping methods.
// In controller
todoMapper.toResponse(todo)
// In service
todoMapper.toEntity(createRequest)
todoMapper.toEvent(todo, EventType.CREATED)
DTOs as Records¶
All DTOs are Java records — immutable, no boilerplate. Request DTOs live under controllers/dto/request/, response DTOs under controllers/dto/response/.
Kafka Events¶
Events flow through three layers:
- messages/ — the event payload (TodoEvent record with an EventType enum)
- producers/ — publishes events via KafkaTemplate
- listeners/ — consumes events via @KafkaListener
Topic names are defined in constants/KafkaTopics.
External HTTP Client¶
JsonPlaceholderClient uses Spring Boot's RestClient to call external APIs. External response DTOs live under integrations/dto/response/ to keep them separate from internal DTOs. The client is not exposed via the controller but demonstrates the integration pattern for use in services.
Redis Caching¶
Cache annotations (@Cacheable, @CachePut, @CacheEvict) are applied at the service layer. Cache name constants live in constants/CacheNames. The Redis serializer config lives in config/RedisCacheConfig.
Exception Handling¶
Modules throw the shared ResourceNotFoundException("Todo", id) instead of defining module-specific exceptions. The global @RestControllerAdvice handler returns RFC 9457 ProblemDetail responses automatically.
Pagination¶
Use the shared PagedResponse<T>.from(Page<T>) wrapper to return paginated results with a consistent shape across all modules.
Shared Utilities¶
These live in modules/shared/ and are reusable across all modules:
| Class | Package | Purpose |
|---|---|---|
PagedResponse<T> |
shared.dto |
Generic paginated response wrapper |
ResourceNotFoundException |
shared.exceptions |
Generic 404 exception for any resource type |
Conventions¶
- Plural package names —
controllers/,services/,entities/(Java/Spring standard) - Constructor injection — no
@Autowiredon fields - OpenAPI annotations —
@Tag,@Operation,@Schemaon controllers and DTOs - Constants over magic strings — API paths, Kafka topics, and cache names are defined in
constants/ - Cross-module imports — Only
servicesandentitiesfrom other modules (and anything fromshared). All other packages are module-private. Enforced by ArchUnit — violations fail the build.
Testing¶
Integration tests use Testcontainers to spin up real PostgreSQL, Redis, and Kafka containers. No mocking of infrastructure — tests hit the full stack.
Test files live under src/test/java/.../modules/sample/controllers/.
Running tests¶
From the project root:
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
./backend/gradlew -p backend test
Or from the service directory:
cd backend
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew test
Prerequisites¶
- Podman (or Docker) must be running — Testcontainers needs a container runtime
- Podman socket is auto-detected by
build.gradle.kts— no manualDOCKER_HOSTsetup needed - Ryuk is disabled automatically for Podman compatibility
Test approach¶
@SpringBootTestwith@AutoConfigureMockMvc— boots the full Spring context@Testcontainersmanages container lifecycle (start before tests, stop after)@DynamicPropertySourcewires container connection details into Spring properties@BeforeEachcleans the database between tests viarepository.deleteAll()
Creating a New Module¶
- Copy this
sample/directory and rename it (e.g.customer/) - Update the package declarations in all files
- Replace
Todowith your entity name - Update constants (API paths, Kafka topics, cache names)
- Add any module-specific config to
config/ - Reuse shared utilities from
modules/shared/