-
Notifications
You must be signed in to change notification settings - Fork 7
Implement Architecture Decision Records (ADRs) #299
Description
Problem
This project makes several opinionated architectural and technology choices (Spring Boot over Quarkus/Micronaut, Spring Data JPA + SQLite, Spring Cache, Lombok, layered architecture, stadium-themed versioning) without documenting the why behind these decisions.
Pain points:
- New contributors/learners must reverse-engineer the rationale behind technology choices
- Architectural context is lost — no record of alternatives considered or trade-offs made
- Risk of uninformed changes — future modifications may violate original design principles without understanding the constraints that shaped them
- Missed learning opportunity — as a PoC demonstrating modern Java patterns, the reasoning behind choices is as valuable as the implementation itself
- Difficult to justify forks/adaptations — others reusing this as a template can't make informed decisions about what to keep vs. change
This is especially problematic for a learning-focused PoC where understanding the "why" is critical for educational value.
Proposed Solution
Implement Architecture Decision Records (ADRs) using the Michael Nygard template to systematically document all architecturally significant decisions.
Benefits:
- Better onboarding — new contributors understand context immediately
- Enhanced learning value — learners see not just what was built, but why
- Informed evolution — future changes can reference past decisions and their constraints
- Knowledge preservation — rationale survives team/contributor changes
- Best practice demonstration — shows proper architecture documentation for production systems
- Easier forking — others can adapt this PoC with full context of original decisions
Suggested Approach
1. Directory Structure
docs/
adr/
README.md # Index and overview
0001-adopt-spring-boot.md
0002-spring-data-jpa-sqlite.md
0003-spring-cache-memory.md
0004-layered-architecture.md
0005-lombok-boilerplate-reduction.md
0006-springdoc-openapi.md
0007-docker-single-container.md
0008-stadium-themed-versioning.md
0009-uuid-surrogate-squad-number-natural-key.md
0010-full-replace-put-no-patch.md
0011-mixed-test-strategy.md
0012-no-flyway-manual-ddl-dml-scripts.md
template.md # ADR template for future decisions
2. ADR Template
Use the Michael Nygard format:
# ADR-NNNN: {Title}
Date: YYYY-MM-DD
## Status
{Proposed | Accepted | Deprecated | Superseded by ADR-XXXX}
## Context
{Describe the forces at play: technical, business, team constraints.
What problem needs solving? What alternatives exist?}
## Decision
{State the decision in full sentences with active voice: "We will..."}
## Consequences
{Describe the resulting context after applying this decision:
- Positive outcomes
- Negative trade-offs
- Neutral implications
All affect the project going forward.}3. Initial ADRs to Create (Retroactive)
ADRs are numbered in the order decisions were made (or, for retroactive records, the order that best reflects foundational-to-incidental progression). Each record is fully self-contained — no ADR depends on another.
ADRs 0001–0008 document existing, already-implemented decisions (status: Accepted):
-
ADR-0001: Adopt Spring Boot as REST API Framework
- Context: Need a production-grade Java framework for a CRUD REST API
- Alternatives: Quarkus, Micronaut, Jakarta EE + Jersey, plain Spring MVC
- Decision: Spring Boot 4.0.0
- Why: Largest ecosystem, auto-configuration, embedded server, wide industry adoption, cross-language comparison favours the dominant framework per ecosystem
-
ADR-0002: Use Spring Data JPA with SQLite
- Context: Need persistence with ORM support and multi-database compatibility
- Alternatives: Plain JDBC, MyBatis, jOOQ, PostgreSQL as primary
- Decision: Spring Data JPA + Hibernate + SQLite (file-based runtime, in-memory for tests)
- Why: Demonstrates JPA patterns, SQLite requires no external service, in-memory variant gives fast isolated tests, community Hibernate dialect bridges SQLite gap
-
ADR-0003: Implement In-Memory Caching with Spring Cache
- Context: Need caching for GET endpoints without external infrastructure
- Alternatives: Redis, Caffeine, Hazelcast, no caching
- Decision: Spring
@Cacheablewith simple in-memory provider (no expiry) - Why: Zero external dependencies, built into Spring Boot, sufficient for PoC, demonstrates the pattern
-
ADR-0004: Adopt Layered Architecture (Controller → Service → Repository → JPA)
- Context: Need clear separation of concerns across HTTP, business, and data layers
- Alternatives: Hexagonal/ports-and-adapters, CQRS, flat structure
- Decision: Strict 3-layer: controllers delegate to services, services own business logic and transactions, repositories handle data access
- Why: Industry-standard Spring idiom, highly testable per layer, appropriate complexity for PoC, matches patterns learners will encounter in real projects
-
ADR-0005: Use Lombok to Reduce Boilerplate
- Context: Java verbosity in entity/DTO classes with getters, setters, constructors, and builders
- Alternatives: Java Records, manual boilerplate, MapStruct only
- Decision: Lombok (
@Data,@Builder,@RequiredArgsConstructor,@AllArgsConstructor) - Why: Dramatically reduces noise, constructor injection via
@RequiredArgsConstructorenforces good DI habits, widely adopted in enterprise Spring projects
-
ADR-0006: Use SpringDoc OpenAPI 3 for API Documentation
- Context: Need interactive, standards-compliant API documentation
- Alternatives: springfox (unmaintained), hand-written OpenAPI YAML, no docs
- Decision: SpringDoc OpenAPI 3 with Swagger UI
- Why: Actively maintained, Spring Boot 3+ compatible, auto-generates from annotations, interactive UI at
/swagger-ui.html
-
ADR-0007: Single-Container Docker Deployment
- Context: Need containerisation strategy for the API
- Alternatives: Docker Compose multi-service, Kubernetes, no containers
- Decision: Single container with a bind-mounted volume for the SQLite file
- Why: Simplicity, appropriate for PoC, demonstrates Docker patterns without orchestration overhead
-
ADR-0008: Use Stadium-Themed Semantic Versioning
- Context: Need a memorable release naming convention consistent with the cross-language comparison series
- Alternatives: Standard semver only, animal names, city names
- Decision: Famous football stadiums A–Z
- Why: Fun, memorable, culturally universal for a football-themed domain, alphabetical progression makes ordering obvious
ADRs 0009–0012 document forward-looking or in-flight decisions. They start as Proposed and are updated to Accepted once the corresponding issue is implemented:
-
ADR-0009: Demote UUID to Surrogate Key and Promote Squad Number to Natural Key (tracks Migrate Player PK from Long to UUID #268)
- Context: The
Playerentity uses a sequentialLongPK — predictable, not globally unique, and semantically wrong. Squad numbers are the stable, domain-meaningful identifier for players within a team. UUID provides global uniqueness without the drawbacks of sequential IDs, but should not be the mutation key because it is opaque to API consumers. - Alternatives: Keep
LongPK, use UUID as PK (exposed in all endpoints), composite key - Decision: UUID becomes a non-PK surrogate (unique, non-updatable, generated at application level via
@PrePersist).squadNumberis promoted to@Id.PUT /players/{squadNumber}andDELETE /players/{squadNumber}use squad number as path variable.GET /players/{id}(UUID) is retained for admin/internal lookup only. - Why: Squad numbers are natural keys — unique per team, stable, and meaningful to consumers. UUID provides global uniqueness for distributed scenarios without exposing sequential counters. Keeping UUID accessible via a dedicated admin endpoint preserves internal traceability without burdening the public API.
- Context: The
-
ADR-0010: Full-Replace PUT, No PATCH (status:
Accepted)- Context: HTTP defines PUT (full replacement) and PATCH (partial update). Both are common in REST APIs. The choice affects the DTO design (all fields required vs optional), the service layer logic (overwrite all vs merge), and the client contract.
- Alternatives: PATCH only, both PUT and PATCH, PUT with optional fields
- Decision: PUT performs a full replacement — the request body represents the complete new state of the resource. No PATCH endpoint is provided.
- Why: Simpler to implement correctly; no merge logic, no ambiguity about absent fields, no need to choose a patch format (JSON Merge Patch vs JSON Patch). For a PoC with a small, flat domain model, requiring the full resource on update is not a meaningful burden for clients. Consistent with the same decision made across the Python and Go sibling projects.
-
ADR-0011: Mixed Test Strategy (MockMvc Unit Tests + In-Memory SQLite Integration Tests) (status:
Accepted)- Context: Testing strategies range from fully mocked unit tests (fast, isolated, risk of mock divergence) to fully integrated tests against a real database (high confidence, slower). Two distinct scenario categories exist: happy paths and expected error branches (400, 404, 409) that can be exercised with a real database, and infrastructure-level error branches (500s from service or repository failures) that require controlled failure injection.
- Alternatives: Unit tests with mocks only, integration tests only, TestContainers with PostgreSQL
- Decision: Use MockMvc + Mockito for controller and service unit tests — including 500-level error branches unreachable with a healthy database. Use in-memory SQLite (auto-cleared after each test) for integration tests covering the full request/response cycle. The boundary is explicit: if a scenario can be triggered with a real database, it uses the real database.
- Why: In-memory SQLite gives near-zero setup cost with real constraint enforcement. Mockito covers error branches that cannot be triggered otherwise. Consistent with Go's mixed strategy. Avoids the false confidence documented in the TypeScript sibling project, where mocked Sequelize tests passed while real queries failed.
-
ADR-0012: No Flyway — Manual DDL/DML Scripts (tracks Implement Flyway for database migrations #130)
- Context: Spring Boot projects typically use Flyway (Apache 2.0) or Liquibase for versioned schema migrations. The alternatives considered: Flyway (versioned, reversible migrations, strong Spring Boot integration), Liquibase (XML/YAML/SQL changelogs, broader database support), manual
ddl.sql/dml.sqlscripts executed at startup or test setup with no migration history. - Alternatives: Flyway, Liquibase, Spring Boot
spring.sql.initwith manual scripts (current) - Decision: Use manual
ddl.sql(schema) anddml.sql(seed data) scripts for now. Migration to Flyway is tracked in issue Implement Flyway for database migrations #130 and will be revisited alongside the PostgreSQL support work in issue Add PostgreSQL support with unified migration-based initialization #286. - Why: For a PoC with a stable schema and a single committed SQLite file, Flyway adds configuration overhead without meaningful benefit today. When PostgreSQL support is introduced (Add PostgreSQL support with unified migration-based initialization #286), versioned migrations become necessary — at that point Flyway is the natural choice given its first-class Spring Boot auto-configuration and Apache 2.0 license.
- Context: Spring Boot projects typically use Flyway (Apache 2.0) or Liquibase for versioned schema migrations. The alternatives considered: Flyway (versioned, reversible migrations, strong Spring Boot integration), Liquibase (XML/YAML/SQL changelogs, broader database support), manual
4. Integration with Existing Docs
- Update
README.md— Add "Architecture Decisions" section linking todocs/adr/ - Update
.github/copilot-instructions.md— Reference ADRs in an "Additional Resources" section - Add guideline — New architecturally significant decisions must include an ADR (note in contributing guidance)
5. File Modifications Required
Create docs/adr/README.md
# Architecture Decision Records (ADR)
This directory contains records of architecturally significant decisions made in this project.
## Index
| ADR | Title | Status |
|-----|-------|--------|
| [ADR-0001](0001-adopt-spring-boot.md) | Adopt Spring Boot as REST API Framework | Accepted |
| [ADR-0002](0002-spring-data-jpa-sqlite.md) | Use Spring Data JPA with SQLite | Accepted |
| [ADR-0003](0003-spring-cache-memory.md) | Implement In-Memory Caching with Spring Cache | Accepted |
| [ADR-0004](0004-layered-architecture.md) | Adopt Layered Architecture | Accepted |
| [ADR-0005](0005-lombok-boilerplate-reduction.md) | Use Lombok to Reduce Boilerplate | Accepted |
| [ADR-0006](0006-springdoc-openapi.md) | Use SpringDoc OpenAPI 3 for API Documentation | Accepted |
| [ADR-0007](0007-docker-single-container.md) | Single-Container Docker Deployment | Accepted |
| [ADR-0008](0008-stadium-themed-versioning.md) | Use Stadium-Themed Versioning | Accepted |
| [ADR-0009](0009-uuid-surrogate-squad-number-natural-key.md) | Demote UUID to Surrogate Key and Promote Squad Number to Natural Key | Proposed |
| [ADR-0010](0010-full-replace-put-no-patch.md) | Full-Replace PUT, No PATCH | Accepted |
| [ADR-0011](0011-mixed-test-strategy.md) | Mixed Test Strategy | Accepted |
| [ADR-0012](0012-no-flyway-manual-ddl-dml-scripts.md) | No Flyway — Manual DDL/DML Scripts | Proposed |
## Resources
- [Michael Nygard's ADR article](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
- [ADR GitHub organization](https://adr.github.io/)
- [Microsoft Azure Well-Architected: ADRs](https://learn.microsoft.com/en-us/azure/well-architected/architect-role/architecture-decision-record)Acceptance Criteria
- Directory structure created:
docs/adr/withREADME.mdandtemplate.md - ADRs 0001–0008, 0010–0011 written with status
Acceptedusing Michael Nygard template format - ADR-0009 written with status
Proposed, referencing issue Migrate Player PK from Long to UUID #268; updated toAcceptedonce Migrate Player PK from Long to UUID #268 is implemented - ADR-0012 written with status
Proposed, referencing issue Implement Flyway for database migrations #130; updated toAcceptedonce Implement Flyway for database migrations #130 is implemented - README.md updated with "Architecture Decisions" section linking to
docs/adr/ -
.github/copilot-instructions.mdupdated with reference to ADRs in an "Additional Resources" section - ADRs are discoverable: reachable via README, copilot instructions, or file explorer
- ADRs are educational: each ADR clearly explains alternatives considered and trade-offs made
- Template provided:
docs/adr/template.mdexists for future ADRs - CHANGELOG.md updated: entry in
[Unreleased]section under "Added"
References
ADR Resources
- Documenting Architecture Decisions — Michael Nygard (2011) — Original blog post that popularised ADRs
- ADR GitHub Organization — Templates, tools, and best practices
- Microsoft Azure Well-Architected: ADRs — Enterprise perspective on maintaining ADRs
- Michael Nygard ADR Template — Canonical template format
Tooling (Optional)
- adr-tools — Command-line tools for managing ADRs
- log4brains — Modern ADR toolchain with architecture knowledge base
Related Issues
- Inspired by nanotaboada/python-samples-fastapi-restful#482
- ADR-0009 tracks the decision described in Migrate Player PK from Long to UUID #268
- ADR-0012 tracks the decision described in Implement Flyway for database migrations #130; will be revisited alongside PostgreSQL support (Add PostgreSQL support with unified migration-based initialization #286)
Note: This feature aligns with the project's philosophy of being a learning-focused PoC by making architectural reasoning explicit and accessible to learners and contributors.