Skip to content

Commit 6a6c841

Browse files
authored
Merge pull request #338 from nanotaboada/docs/299-architecture-decision-records
docs(adr): implement Architecture Decision Records (#299)
2 parents 2eeff98 + daafc31 commit 6a6c841

17 files changed

Lines changed: 357 additions & 0 deletions

.github/copilot-instructions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,8 @@ feat(scope): description (#issue)
141141
142142
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
143143
```
144+
145+
## Additional Resources
146+
147+
- **Architecture Decision Records**: [`docs/adr/`](../docs/adr/README.md) — 12 ADRs documenting the "why" behind major architectural and technology choices in this project.
148+
- New architecturally significant decisions (framework changes, persistence strategy, API contract changes, test strategy shifts) should include a new ADR in `docs/adr/` following the template in [`docs/adr/template.md`](../docs/adr/template.md).

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Release names follow the **historic football clubs** naming convention (A–Z):
4242

4343
### Added
4444

45+
- Architecture Decision Records (ADRs) documenting 12 architectural decisions in `docs/adr/` (#299)
46+
4547
### Changed
4648

4749
- Refactor `/pre-release` Phase 2: inline build and test steps directly

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ graph RL
136136

137137
> *Arrows follow the injection direction (A → B means A is injected into B). Solid = runtime dependency, dotted = structural. Blue = core domain, red = third-party, green = tests.*
138138
139+
## Architecture Decisions
140+
141+
Architecturally significant decisions are documented as Architecture Decision Records (ADRs) in [`docs/adr/`](docs/adr/README.md). Each record captures the context, the alternatives considered, and the trade-offs of the choice — the "why" behind the implementation.
142+
143+
| ADR | Decision |
144+
|-----|----------|
145+
| [ADR-0001](docs/adr/0001-adopt-spring-boot.md) | Adopt Spring Boot as REST API Framework |
146+
| [ADR-0002](docs/adr/0002-spring-data-jpa-sqlite.md) | Use Spring Data JPA with SQLite |
147+
| [ADR-0003](docs/adr/0003-spring-cache-memory.md) | Implement In-Memory Caching with Spring Cache |
148+
| [ADR-0004](docs/adr/0004-layered-architecture.md) | Adopt Layered Architecture |
149+
150+
See the [full ADR index](docs/adr/README.md) for all 12 records.
151+
139152
## API Reference
140153

141154
Interactive API documentation is available via Swagger UI at `http://localhost:9000/swagger/index.html` when the server is running.

docs/adr/0001-adopt-spring-boot.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ADR-0001: Adopt Spring Boot as REST API Framework
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The project needs a production-grade Java framework for building a CRUD REST API. The Java ecosystem offers several mature options: Quarkus (GraalVM-native-first, reactive), Micronaut (compile-time DI, fast startup), Jakarta EE + Jersey (standard but verbose), and plain Spring MVC without Boot (full control, high configuration cost). The project is a proof of concept and learning reference — part of a cross-language comparison series — so ecosystem familiarity and discoverability matter as much as raw performance.
12+
13+
## Decision
14+
15+
We will use Spring Boot 4.0.0 targeting JDK 25 LTS as the REST API framework.
16+
17+
## Consequences
18+
19+
- Spring Boot's auto-configuration dramatically reduces bootstrap code; a working API is reachable with minimal configuration.
20+
- The embedded Tomcat server removes external server setup, simplifying local development and containerisation.
21+
- Spring Boot has the largest community, the most StackOverflow answers, and the widest industry adoption of any Java framework — learners and contributors encounter familiar patterns.
22+
- The cross-language comparison series favours the dominant framework in each ecosystem; Spring Boot is the canonical Java choice.
23+
- Spring Boot's opinionated defaults can be overridden but require explicit effort; contributors unfamiliar with autoconfiguration may be surprised by what is wired automatically.
24+
- Spring Boot 4.0 requires JDK 17+ and drops several legacy APIs, which constrains the minimum runtime version.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ADR-0002: Use Spring Data JPA with SQLite
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The project requires a persistence layer with ORM support and the ability to run without an external database service. Options considered: plain JDBC (low-level, no ORM), MyBatis (SQL-centric, half-ORM), jOOQ (type-safe SQL DSL, commercial for some databases), Hibernate standalone (ORM without Spring Data), Spring Data JPA + Hibernate (full abstraction), and PostgreSQL as the primary database. SQLite is file-based and requires no server process, making it ideal for a self-contained PoC. An in-memory SQLite variant allows fast, isolated test runs without any cleanup logic.
12+
13+
## Decision
14+
15+
We will use Spring Data JPA backed by Hibernate with SQLite as the database — file-based at runtime and in-memory for tests.
16+
17+
## Consequences
18+
19+
- Spring Data JPA derived queries (`findBySquadNumber`, `findByLeague`) replace hand-written SQL for standard CRUD operations, demonstrating the pattern without boilerplate.
20+
- SQLite requires no external service or Docker dependency, making local setup a single `./mvnw spring-boot:run`.
21+
- In-memory SQLite (`:memory:`) auto-clears between test runs, providing isolation without `@Transactional` rollback tricks or manual truncation.
22+
- The community Hibernate SQLite dialect bridges the gap between Hibernate's standard DDL generation and SQLite's type system — this is a third-party dependency not maintained by Hibernate.
23+
- SQLite's concurrency model (single writer) is not representative of production JPA targets such as PostgreSQL; migration to a server-based database will require dialect and configuration changes.
24+
- JPA abstractions hide SQL, which can produce unexpected query plans (N+1, missing indexes). In a simple, flat domain this is not a practical concern, but learners should be aware of the trade-off.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# ADR-0003: Implement In-Memory Caching with Spring Cache
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
GET endpoints benefit from caching to avoid repeated database reads for stable data. Options considered: Redis (external server, TTL support, distributed), Caffeine (local, configurable TTL and eviction, no external process), Hazelcast (distributed, clustering), and Spring's built-in simple `ConcurrentHashMap`-backed provider (zero configuration, no expiry). The project is a PoC with no high-availability requirement, and adding an external cache service increases setup friction without a meaningful payoff.
12+
13+
## Decision
14+
15+
We will use Spring's `@Cacheable` abstraction with the default simple in-memory provider (backed by `ConcurrentHashMap`). No TTL or eviction policy is configured.
16+
17+
## Consequences
18+
19+
- Zero external dependencies: the cache works out of the box with no additional configuration or infrastructure.
20+
- The `@Cacheable`, `@CachePut`, and `@CacheEvict` annotations are placed on service methods, demonstrating the Spring caching pattern in a realistic but minimal way.
21+
- Switching to Caffeine or Redis in the future requires only a dependency addition and property changes; the annotation-based API remains the same.
22+
- With no expiry, cached data reflects the state at the time of the first load. Updates via PUT trigger `@CacheEvict`, but a restart is required to clear the cache fully in production.
23+
- In-memory cache state is lost on restart and is not shared across instances — not suitable for horizontally scaled deployments.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# ADR-0004: Adopt Layered Architecture
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The project needs a structural pattern that separates HTTP concerns, business logic, and data access. Options considered: hexagonal (ports and adapters) architecture (technology-agnostic domain, higher complexity), CQRS (separate read/write models, overkill for simple CRUD), vertical slice architecture (feature-per-folder, unfamiliar to most Spring Boot learners), and the classic 3-layer model (Controller → Service → Repository) which maps directly to Spring Boot's stereotype annotations.
12+
13+
## Decision
14+
15+
We will adopt a strict 3-layer architecture: controllers handle HTTP and delegate to services; services own business logic and transaction boundaries; repositories handle data access. No layer may skip the one immediately below it — controllers must not access repositories directly.
16+
17+
## Consequences
18+
19+
- The structure maps 1:1 to Spring Boot's `@RestController`, `@Service`, and `@Repository` stereotypes, making it immediately recognisable to any Spring Boot practitioner.
20+
- Each layer is independently testable: controllers via MockMvc + Mockito-mocked services, services via unit tests with mocked repositories, repositories via in-memory SQLite integration tests.
21+
- The layer rule is enforced by convention and code review, not by the compiler or a framework boundary — a motivated developer can still inject a repository into a controller.
22+
- For the flat, CRUD-only domain in this project (26 players, no aggregate complexity), a hexagonal or onion architecture would add indirection without benefit.
23+
- As domain complexity grows, the service layer risks becoming a thin pass-through or a bloated transaction script. At that point, domain-driven patterns would be worth revisiting.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# ADR-0005: Use Lombok to Reduce Boilerplate
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
Java entity and DTO classes require getters, setters, constructors, `equals`/`hashCode`, and `toString` — several dozen lines of mechanical code per class. Options considered: Java Records (immutable, no setters, JPA requires a no-arg constructor), manual boilerplate (verbose, noisy diffs), MapStruct-only (covers mapping but not field access), and Lombok (annotation-driven code generation at compile time, widely adopted in enterprise Spring projects).
12+
13+
## Decision
14+
15+
We will use Lombok with `@Data` on DTOs (getters, setters, equals, hashCode, toString), `@Builder` where builder construction is needed, `@RequiredArgsConstructor` for constructor injection on Spring beans, and `@AllArgsConstructor` on entity/DTO classes that need a full-argument constructor.
16+
17+
## Consequences
18+
19+
- `@RequiredArgsConstructor` on `@Service` and `@RestController` classes generates a constructor for all `final` fields, enforcing constructor injection without writing the constructor manually.
20+
- Entity and DTO classes remain short and readable; diffs show domain changes rather than boilerplate churn.
21+
- Lombok requires IDE annotation-processing support (IntelliJ IDEA, Eclipse). Without it, the generated methods are invisible to the IDE and appear as compilation errors.
22+
- Java Records are a compelling alternative for immutable DTOs but cannot be JPA entities without workarounds. Lombok is the pragmatic choice until JPA tooling for records matures.
23+
- Lombok uses compile-time bytecode manipulation; updates to JDK internals (as seen in JDK 16+ access controls) have occasionally broken Lombok and required a new release. This dependency on Lombok's release cadence is an accepted trade-off.

docs/adr/0006-springdoc-openapi.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# ADR-0006: Use SpringDoc OpenAPI 3 for API Documentation
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The API needs interactive, standards-compliant documentation that stays in sync with the implementation. Options considered: springfox (historically popular, unmaintained since 2020, incompatible with Spring Boot 3+), hand-written OpenAPI YAML (accurate but manual and drift-prone), no documentation (insufficient for a learning-focused PoC), and SpringDoc OpenAPI 3 (actively maintained, auto-generates from Spring MVC annotations, Spring Boot 3+/4+ compatible).
12+
13+
## Decision
14+
15+
We will use SpringDoc OpenAPI 3 to generate the OpenAPI specification and serve the Swagger UI at `/swagger/index.html`. The JSON spec is available at `/docs`.
16+
17+
## Consequences
18+
19+
- The OpenAPI spec is derived from `@Operation`, `@ApiResponse`, and controller annotations — documentation is co-located with the code it describes and stays accurate as endpoints change.
20+
- Swagger UI provides an interactive testing interface without any external tooling, useful for learners exploring the API.
21+
- SpringDoc is actively maintained and explicitly supports Spring Boot 3+ and 4+, unlike springfox.
22+
- Auto-generation from annotations produces verbose output; `@Operation` and schema annotations add noise to controller code. This is an accepted trade-off for a project where documentation is a first-class concern.
23+
- The Swagger UI path (`/swagger/index.html`) and spec path (`/docs`) are configured in `application.properties` and can be changed without code modifications.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ADR-0007: Single-Container Docker Deployment
2+
3+
Date: 2026-06-07
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The project requires a containerisation strategy. Options considered: Docker Compose multi-service (separate containers for the API and a database server — appropriate for PostgreSQL but unnecessary for SQLite), Kubernetes (orchestration overhead far exceeds PoC requirements), multi-stage build without Compose (single image, no volume mount), and a single container with a bind-mounted volume for the SQLite database file.
12+
13+
## Decision
14+
15+
We will deploy a single Docker container built with a multi-stage Dockerfile. The SQLite database file is persisted via a bind-mounted volume so that data survives container restarts. `docker compose up` is the standard invocation.
16+
17+
## Consequences
18+
19+
- A single container is the simplest possible deployment unit; contributors only need Docker installed, not a database server.
20+
- The multi-stage build (builder + runtime image) keeps the final image small by excluding Maven and the JDK build toolchain.
21+
- The bind-mounted volume (`./storage`) persists the SQLite file on the host, making data recoverable without entering the container.
22+
- The `docker compose down -v` command removes the volume and resets data — callers must be intentional about this.
23+
- When PostgreSQL support is added in the future, the single-container model will need to evolve to a multi-service Compose file. This ADR will be superseded at that point.
24+
- This strategy is not representative of production Java deployments, which typically externalise the database. That is an accepted trade-off for a PoC.

0 commit comments

Comments
 (0)