Skip to content

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 namescontrollers/, services/, entities/ (Java/Spring standard)
  • Constructor injection — no @Autowired on fields
  • OpenAPI annotations@Tag, @Operation, @Schema on controllers and DTOs
  • Constants over magic strings — API paths, Kafka topics, and cache names are defined in constants/
  • Cross-module imports — Only services and entities from other modules (and anything from shared). 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 manual DOCKER_HOST setup needed
  • Ryuk is disabled automatically for Podman compatibility

Test approach

  • @SpringBootTest with @AutoConfigureMockMvc — boots the full Spring context
  • @Testcontainers manages container lifecycle (start before tests, stop after)
  • @DynamicPropertySource wires container connection details into Spring properties
  • @BeforeEach cleans the database between tests via repository.deleteAll()

Creating a New Module

  1. Copy this sample/ directory and rename it (e.g. customer/)
  2. Update the package declarations in all files
  3. Replace Todo with your entity name
  4. Update constants (API paths, Kafka topics, cache names)
  5. Add any module-specific config to config/
  6. Reuse shared utilities from modules/shared/