This is a Spring Boot REST API for managing football players, demonstrating modern Java patterns and clean architecture. It's a learning-focused proof-of-concept that follows Spring Boot best practices with layered architecture (Controller → Service → Repository).
- Java: 25 (LTS, required for consistency)
- Spring Boot: 4.0.0 (with Spring MVC)
- ORM: Spring Data JPA with Hibernate
- Database: SQLite (file-based for runtime, in-memory for tests)
- Build Tool: Maven 3 (use
./mvnwwrapper, NOT system Maven) - Containers: Docker with Docker Compose
- Logging: SLF4J with Logback
- Monitoring: Spring Boot Actuator
- API Docs: SpringDoc OpenAPI 3 (Swagger UI)
- Layered architecture: Controller → Service → Repository → Database
- Dependency injection: Constructor injection via Lombok's @RequiredArgsConstructor
- DTO pattern: Never expose entities in controllers, use DTOs with ModelMapper
- Caching: Spring Cache abstraction (@Cacheable) with 1-hour TTL
- Error handling: @ControllerAdvice for global exception handling
- Validation: Bean Validation (JSR-380) in DTOs
- Repository queries: Mix of derived queries (findBySquadNumber) and custom JPQL (@Query)
- Date handling: ISO-8601 strings with custom JPA converter for SQLite compatibility
- Classes: PascalCase (e.g.,
PlayersController,PlayerDTO) - Methods/Variables: camelCase (e.g.,
findById,squadNumber) - Test methods:
method_scenario_outcome(e.g.,post_squadNumberExists_returnsConflict)
- Controllers: @RestController (never @Controller for REST APIs)
- DTOs: Lombok @Data, @Builder, @AllArgsConstructor
- Services: @Service + @Transactional on mutating methods
- Repositories: @Repository (extend JpaRepository)
- Entities: @Entity with @Table, @Id, @GeneratedValue
- Reduce boilerplate with @Data, @Builder, @AllArgsConstructor
- Use @RequiredArgsConstructor for constructor injection
- Never use field injection
- Return ResponseEntity from controllers
- Use proper HTTP status codes (201 Created, 204 No Content, 409 Conflict)
- OpenAPI annotations required on all endpoints
- Use SLF4J (injected via Lombok @Slf4j)
- Never use System.out.println
- Externalize via @Value or application.yml
- Never hardcode values in code
- Unit tests: JUnit 5 with AssertJ for fluent assertions
- Integration tests: @SpringBootTest with MockMvc
- Coverage: JaCoCo reports (must maintain high coverage)
Pattern: givenX_whenY_thenZ() (BDD Given-When-Then)
Naming philosophy:
Use semantic naming that describes the business/domain state and behavior, not technical implementation details:
- Given (precondition): Describe the state of the system or data
givenPlayerExists- A player is present in the systemgivenNoExistingPlayer- No player matching criteriagivenInvalidPlayer- Invalid data providedgivenRaceCondition- Concurrent modification scenariogivenNullId- Missing required identifier
- When (action): The method/operation being tested
whenCreate- Creating a new entitywhenRetrieveById- Fetching by identifierwhenUpdate- Updating existing entitywhenDelete- Removing an entitywhenSearchByLeague- Searching/filtering operation
- Then (outcome): Expected result or behavior
thenReturnsPlayerDTO- Returns the DTO objectthenReturnsNull- Returns null (not found/conflict)thenReturnsTrue/thenReturnsFalse- Boolean success indicatorthenReturns26Players- Specific count of resultsthenReturnsEmptyList- No results foundthenReturnsOk- HTTP 200 responsethenReturnsNotFound- HTTP 404 responsethenReturnsCreated- HTTP 201 response
Code conventions:
- Comments: Use Given/When/Then (BDD pattern)
- Assertions: Use
then()from AssertJ BDDAssertions - Variables:
- Use
actualfor operation results - Use
expectedfor comparison values when verifying equality - Use
existingfor pre-saved entities in database
- Use
Why BDD Given-When-Then?
- ✅ Readable: Natural language flow, accessible to all stakeholders
- ✅ Behavior-focused: Tests business logic, not implementation details
- ✅ Self-documenting: Method name clearly states test scenario
- ✅ Framework-aligned: Matches Cucumber/SpecFlow patterns
// Examples across layers:
givenNoExistingPlayer_whenCreate_thenReturnsPlayerDTO() // Service: success case
givenPlayerAlreadyExists_whenCreate_thenReturnsNull() // Service: conflict case
givenPlayerExists_whenFindById_thenReturnsPlayer() // Repository: found
givenPlayerDoesNotExist_whenFindById_thenReturnsEmpty() // Repository: not found
givenValidPlayer_whenPost_thenReturnsCreated() // Controller: HTTP 201
givenPlayerDoesNotExist_whenGetById_thenReturnsNotFound() // Controller: HTTP 404Use BDD (Given/When/Then) consistently in JavaDoc comments, method names, and code sections:
/**
* Given no existing player with the same squad number
* When create() is called with valid player data
* Then the player is saved and a PlayerDTO is returned
*/
@Test
void givenNoExistingPlayer_whenCreate_thenReturnsPlayerDTO() {
// Given
Player player = PlayerFakes.createOneValid();
PlayerDTO expected = PlayerDTOFakes.createOneValid();
// When
PlayerDTO actual = playersService.create(expected);
// Then
then(actual).isEqualTo(expected);
}- Unit tests required for all service and repository methods
- Integration tests required for all controller endpoints
- Tests use in-memory SQLite (jdbc:sqlite::memory:)
- All tests must pass before PR (
./mvnw clean install) - Assertion quality: Verify actual behavior, not just counts (e.g., verify league content, not just
hasSize())
- Field injection - Always use constructor injection
- Using
newfor Spring beans - Breaks dependency injection - Missing @Transactional - Required on service methods that modify data
- Exposing entities in controllers - Always use DTOs
- System.out.println - Use SLF4J logging
- Hardcoded config - Use @Value or application.yml
- System Maven - Always use
./mvnwwrapper for consistency - SQLite in production - This is a demo/development-only setup
src/main/java/ # Application code
├── Application.java # @SpringBootApplication entry point
├── controllers/ # REST endpoints
├── services/ # Business logic + caching
├── repositories/ # Data access (Spring Data JPA)
├── models/ # Entities (Player) and DTOs (PlayerDTO)
└── converters/ # JPA converters (ISO date handling)
src/test/java/ # Test classes (mirrors main structure)
storage/ # SQLite database file (runtime)
scripts/ # Docker entrypoint and healthcheck
# Development
./mvnw spring-boot:run # Run with hot reload
./mvnw clean test jacoco:report # Test with coverage
# Docker
docker compose up # Start in container
# Documentation
http://localhost:8080/swagger-ui.html # API docs
http://localhost:8080/actuator/health # Health checkFollow Conventional Commits with issue number suffix:
- Format:
type(scope): description (#issue)(max 80 chars) - Types: feat, fix, chore, docs, test, refactor
- Example:
feat(api): add squad number search endpoint (#123)
For detailed operational procedures, workflows, and troubleshooting, see AGENTS.md.