From 6c2878d0b5c44b437c659c3e2ea73722bd72ce51 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Mon, 8 Jun 2026 01:47:37 -0300 Subject: [PATCH 1/3] docs(adr): implement Architecture Decision Records (#299) Co-authored-by: Claude Sonnet 4.6 --- .github/copilot-instructions.md | 5 ++++ CHANGELOG.md | 2 ++ README.md | 13 +++++++++ docs/adr/0001-adopt-spring-boot.md | 24 ++++++++++++++++ docs/adr/0002-spring-data-jpa-sqlite.md | 24 ++++++++++++++++ docs/adr/0003-spring-cache-memory.md | 23 +++++++++++++++ docs/adr/0004-layered-architecture.md | 23 +++++++++++++++ docs/adr/0005-lombok-boilerplate-reduction.md | 23 +++++++++++++++ docs/adr/0006-springdoc-openapi.md | 23 +++++++++++++++ docs/adr/0007-docker-single-container.md | 24 ++++++++++++++++ docs/adr/0008-stadium-themed-versioning.md | 24 ++++++++++++++++ ...uuid-surrogate-squad-number-natural-key.md | 24 ++++++++++++++++ docs/adr/0010-full-replace-put-no-patch.md | 24 ++++++++++++++++ docs/adr/0011-mixed-test-strategy.md | 24 ++++++++++++++++ docs/adr/0012-adopt-flyway-migrations.md | 24 ++++++++++++++++ docs/adr/README.md | 28 +++++++++++++++++++ docs/adr/template.md | 24 ++++++++++++++++ 17 files changed, 356 insertions(+) create mode 100644 docs/adr/0001-adopt-spring-boot.md create mode 100644 docs/adr/0002-spring-data-jpa-sqlite.md create mode 100644 docs/adr/0003-spring-cache-memory.md create mode 100644 docs/adr/0004-layered-architecture.md create mode 100644 docs/adr/0005-lombok-boilerplate-reduction.md create mode 100644 docs/adr/0006-springdoc-openapi.md create mode 100644 docs/adr/0007-docker-single-container.md create mode 100644 docs/adr/0008-stadium-themed-versioning.md create mode 100644 docs/adr/0009-uuid-surrogate-squad-number-natural-key.md create mode 100644 docs/adr/0010-full-replace-put-no-patch.md create mode 100644 docs/adr/0011-mixed-test-strategy.md create mode 100644 docs/adr/0012-adopt-flyway-migrations.md create mode 100644 docs/adr/README.md create mode 100644 docs/adr/template.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 79661aa..3ca2f34 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -141,3 +141,8 @@ feat(scope): description (#issue) Co-authored-by: Claude Sonnet 4.6 ``` + +## Additional Resources + +- **Architecture Decision Records**: [`docs/adr/`](../docs/adr/README.md) — 12 ADRs documenting the "why" behind major architectural and technology choices in this project. +- 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). diff --git a/CHANGELOG.md b/CHANGELOG.md index 11be496..2715904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ Release names follow the **historic football clubs** naming convention (A–Z): ### Added +- Architecture Decision Records (ADRs) documenting 12 architectural decisions in `docs/adr/` (#299) + ### Changed - Refactor `/pre-release` Phase 2: inline build and test steps directly diff --git a/README.md b/README.md index 08f4ade..f613e2f 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,19 @@ graph RL > *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.* +## Architecture Decisions + +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. + +| ADR | Decision | +|-----|----------| +| [ADR-0001](docs/adr/0001-adopt-spring-boot.md) | Adopt Spring Boot as REST API Framework | +| [ADR-0002](docs/adr/0002-spring-data-jpa-sqlite.md) | Use Spring Data JPA with SQLite | +| [ADR-0003](docs/adr/0003-spring-cache-memory.md) | Implement In-Memory Caching with Spring Cache | +| [ADR-0004](docs/adr/0004-layered-architecture.md) | Adopt Layered Architecture | + +See the [full ADR index](docs/adr/README.md) for all 12 records. + ## API Reference Interactive API documentation is available via Swagger UI at `http://localhost:9000/swagger/index.html` when the server is running. diff --git a/docs/adr/0001-adopt-spring-boot.md b/docs/adr/0001-adopt-spring-boot.md new file mode 100644 index 0000000..0b5a9ce --- /dev/null +++ b/docs/adr/0001-adopt-spring-boot.md @@ -0,0 +1,24 @@ +# ADR-0001: Adopt Spring Boot as REST API Framework + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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. + +## Decision + +We will use Spring Boot 4.0.0 targeting JDK 25 LTS as the REST API framework. + +## Consequences + +- Spring Boot's auto-configuration dramatically reduces bootstrap code; a working API is reachable with minimal configuration. +- The embedded Tomcat server removes external server setup, simplifying local development and containerisation. +- 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. +- The cross-language comparison series favours the dominant framework in each ecosystem; Spring Boot is the canonical Java choice. +- Spring Boot's opinionated defaults can be overridden but require explicit effort; contributors unfamiliar with autoconfiguration may be surprised by what is wired automatically. +- Spring Boot 4.0 requires JDK 17+ and drops several legacy APIs, which constrains the minimum runtime version. diff --git a/docs/adr/0002-spring-data-jpa-sqlite.md b/docs/adr/0002-spring-data-jpa-sqlite.md new file mode 100644 index 0000000..8a2e7d0 --- /dev/null +++ b/docs/adr/0002-spring-data-jpa-sqlite.md @@ -0,0 +1,24 @@ +# ADR-0002: Use Spring Data JPA with SQLite + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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. + +## Decision + +We will use Spring Data JPA backed by Hibernate with SQLite as the database — file-based at runtime and in-memory for tests. + +## Consequences + +- Spring Data JPA derived queries (`findBySquadNumber`, `findByLeague`) replace hand-written SQL for standard CRUD operations, demonstrating the pattern without boilerplate. +- SQLite requires no external service or Docker dependency, making local setup a single `./mvnw spring-boot:run`. +- In-memory SQLite (`:memory:`) auto-clears between test runs, providing isolation without `@Transactional` rollback tricks or manual truncation. +- 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. +- 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. +- 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. diff --git a/docs/adr/0003-spring-cache-memory.md b/docs/adr/0003-spring-cache-memory.md new file mode 100644 index 0000000..68475ce --- /dev/null +++ b/docs/adr/0003-spring-cache-memory.md @@ -0,0 +1,23 @@ +# ADR-0003: Implement In-Memory Caching with Spring Cache + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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. + +## Decision + +We will use Spring's `@Cacheable` abstraction with the default simple in-memory provider (backed by `ConcurrentHashMap`). No TTL or eviction policy is configured. + +## Consequences + +- Zero external dependencies: the cache works out of the box with no additional configuration or infrastructure. +- The `@Cacheable`, `@CachePut`, and `@CacheEvict` annotations are placed on service methods, demonstrating the Spring caching pattern in a realistic but minimal way. +- Switching to Caffeine or Redis in the future requires only a dependency addition and property changes; the annotation-based API remains the same. +- 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. +- In-memory cache state is lost on restart and is not shared across instances — not suitable for horizontally scaled deployments. diff --git a/docs/adr/0004-layered-architecture.md b/docs/adr/0004-layered-architecture.md new file mode 100644 index 0000000..4abb7b0 --- /dev/null +++ b/docs/adr/0004-layered-architecture.md @@ -0,0 +1,23 @@ +# ADR-0004: Adopt Layered Architecture + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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. + +## Decision + +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. + +## Consequences + +- The structure maps 1:1 to Spring Boot's `@RestController`, `@Service`, and `@Repository` stereotypes, making it immediately recognisable to any Spring Boot practitioner. +- 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. +- 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. +- For the flat, CRUD-only domain in this project (26 players, no aggregate complexity), a hexagonal or onion architecture would add indirection without benefit. +- 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. diff --git a/docs/adr/0005-lombok-boilerplate-reduction.md b/docs/adr/0005-lombok-boilerplate-reduction.md new file mode 100644 index 0000000..b6e4c5d --- /dev/null +++ b/docs/adr/0005-lombok-boilerplate-reduction.md @@ -0,0 +1,23 @@ +# ADR-0005: Use Lombok to Reduce Boilerplate + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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). + +## Decision + +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. + +## Consequences + +- `@RequiredArgsConstructor` on `@Service` and `@RestController` classes generates a constructor for all `final` fields, enforcing constructor injection without writing the constructor manually. +- Entity and DTO classes remain short and readable; diffs show domain changes rather than boilerplate churn. +- Lombok requires IDE annotation-processing support (IntelliJ IDEA, Eclipse). Without it, the generated methods are invisible to the IDE and appear as compilation errors. +- 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. +- 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. diff --git a/docs/adr/0006-springdoc-openapi.md b/docs/adr/0006-springdoc-openapi.md new file mode 100644 index 0000000..d1854e1 --- /dev/null +++ b/docs/adr/0006-springdoc-openapi.md @@ -0,0 +1,23 @@ +# ADR-0006: Use SpringDoc OpenAPI 3 for API Documentation + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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). + +## Decision + +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`. + +## Consequences + +- 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. +- Swagger UI provides an interactive testing interface without any external tooling, useful for learners exploring the API. +- SpringDoc is actively maintained and explicitly supports Spring Boot 3+ and 4+, unlike springfox. +- 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. +- The Swagger UI path (`/swagger/index.html`) and spec path (`/docs`) are configured in `application.properties` and can be changed without code modifications. diff --git a/docs/adr/0007-docker-single-container.md b/docs/adr/0007-docker-single-container.md new file mode 100644 index 0000000..a31df0b --- /dev/null +++ b/docs/adr/0007-docker-single-container.md @@ -0,0 +1,24 @@ +# ADR-0007: Single-Container Docker Deployment + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +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. + +## Decision + +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. + +## Consequences + +- A single container is the simplest possible deployment unit; contributors only need Docker installed, not a database server. +- The multi-stage build (builder + runtime image) keeps the final image small by excluding Maven and the JDK build toolchain. +- The bind-mounted volume (`./storage`) persists the SQLite file on the host, making data recoverable without entering the container. +- The `docker compose down -v` command removes the volume and resets data — callers must be intentional about this. +- 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. +- This strategy is not representative of production Java deployments, which typically externalise the database. That is an accepted trade-off for a PoC. diff --git a/docs/adr/0008-stadium-themed-versioning.md b/docs/adr/0008-stadium-themed-versioning.md new file mode 100644 index 0000000..c10930b --- /dev/null +++ b/docs/adr/0008-stadium-themed-versioning.md @@ -0,0 +1,24 @@ +# ADR-0008: Use Stadium-Themed Versioning + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +The project uses Semantic Versioning (MAJOR.MINOR.PATCH) for release numbers. Release names — appended as a suffix to the version tag — need a convention that is memorable, consistent across the six sibling repositories in the cross-language comparison series, and thematically appropriate for a football-domain project. Options considered: standard semver only (no names), animal names (arbitrary, no thematic link), city names (too generic), and names drawn from the football domain (players, coaches, clubs, stadiums). + +## Decision + +We will use historically significant football clubs in alphabetical order as release codenames, appended to the semver tag (e.g. `v2.0.0-barcelona`, `v2.0.1-chelsea`). The same A–Z sequence is applied consistently across all six sibling repositories in the comparison series. + +## Consequences + +- The alphabetical progression makes release ordering unambiguous at a glance, even without reading dates. +- The football-club theme is culturally universal and directly related to the project's domain (Argentina 2022 FIFA World Cup squad). +- The convention is shared across all six language repositories, making cross-project comparisons easy to orient in time. +- The codename is appended as a hyphenated suffix to the version tag. Git tags, CHANGELOG entries, Docker image tags, and release notes all include the codename for consistency. +- The convention is purely cosmetic — semver carries the semantic meaning. Contributors should not infer anything about the release scope from the club name. +- The A–Z sequence has 26 slots before a wrap-around or convention change is needed. diff --git a/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md b/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md new file mode 100644 index 0000000..8a246dd --- /dev/null +++ b/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md @@ -0,0 +1,24 @@ +# ADR-0009: Demote UUID to Surrogate Key and Promote Squad Number to Natural Key + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +The `Player` entity originally used a sequential `Long` as its primary key — predictable, not globally unique, and semantically meaningless in the football domain. Squad numbers are the stable, domain-meaningful identifier for players within a team. UUID provides global uniqueness without the predictability of sequential counters, but it is opaque to API consumers and not a natural mutation key. The original design exposed `id` (Long) in all mutation endpoints, which is an anti-pattern: internal surrogate keys should not leak into the public API contract. Implemented in v2.0.0-barcelona (issue #268). + +## Decision + +We will demote UUID to a non-primary surrogate key — unique, non-updatable, generated at the application level via `@PrePersist`. `squadNumber` becomes the `@Id` (primary key) and is used as the path variable for `PUT /players/{squadNumber}` and `DELETE /players/{squadNumber}`. A `GET /players/{id}` endpoint (UUID) is retained for internal/admin lookup. + +## Consequences + +- Squad numbers are natural keys: unique per team, stable over a career, and meaningful to API consumers. Using them as path variables makes URLs readable (`/players/10`) rather than opaque (`/players/550e8400-e29b-41d4-a716-446655440000`). +- UUID as a surrogate key provides global uniqueness for distributed or federated scenarios without exposing sequential counters that hint at dataset size. +- The `@PrePersist`-generated UUID is set once on creation and never updated, ensuring immutability of the surrogate. +- Retaining `GET /players/{id}` (UUID) preserves internal traceability and supports integration with external systems that may store the UUID. +- Squad numbers are stable within a single tournament squad but may change across seasons. For this PoC (fixed 2022 World Cup squad), stability is guaranteed. A more general player registry would need to account for squad number reuse. +- The `squadNumber` field carries a `@Column(unique = true)` constraint enforced at the database level, providing a 409 Conflict response on duplicate POST. diff --git a/docs/adr/0010-full-replace-put-no-patch.md b/docs/adr/0010-full-replace-put-no-patch.md new file mode 100644 index 0000000..9a7d245 --- /dev/null +++ b/docs/adr/0010-full-replace-put-no-patch.md @@ -0,0 +1,24 @@ +# ADR-0010: Full-Replace PUT, No PATCH + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +HTTP defines PUT (full replacement of a resource) and PATCH (partial update). The choice affects DTO design (all fields required vs. optional), service layer logic (overwrite all vs. merge), error handling (what does an absent field mean?), and client contract. Options considered: PATCH only (requires a patch format — JSON Merge Patch or JSON Patch — and merge logic), both PUT and PATCH (doubles the endpoint surface and the test matrix), PUT with optional fields (ambiguous: does an absent field mean "keep current value" or "set to null?"), and PUT as a strict full replacement with all fields required. + +## Decision + +We will implement PUT as a full replacement: the request body represents the complete new state of the resource. No PATCH endpoint is provided. + +## Consequences + +- No merge logic is required in the service layer: the existing entity is replaced wholesale with the incoming DTO's values. +- All DTO fields carry Bean Validation constraints, making partial updates a client-side concern (load current state, modify, submit full body). +- The contract is unambiguous: absent fields are not allowed, not silently ignored. +- Consistent with the same decision made in the Python/FastAPI and Go/Gin sibling repositories in the cross-language comparison series. +- Clients must fetch the full current state before making a partial modification, which results in an extra GET call for field-level updates. For a PoC with a small, flat domain, this is not a meaningful burden. +- If partial updates are needed in the future, a PATCH endpoint can be added as a non-breaking addition without changing the existing PUT semantics. diff --git a/docs/adr/0011-mixed-test-strategy.md b/docs/adr/0011-mixed-test-strategy.md new file mode 100644 index 0000000..a23a310 --- /dev/null +++ b/docs/adr/0011-mixed-test-strategy.md @@ -0,0 +1,24 @@ +# ADR-0011: Mixed Test Strategy + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +Testing strategies range from fully mocked unit tests (fast, isolated, risk of mock/production divergence) to fully integrated tests against a real database (high confidence, slower). Two distinct scenario categories exist: happy paths and expected error branches (400 Bad Request, 404 Not Found, 409 Conflict) that can be triggered with a real database and real constraints; and infrastructure-level error branches (500 Internal Server Error from service or repository failures) that require controlled failure injection because a healthy database will not produce them. Options considered: mocks only (fast but false confidence — demonstrated as a risk in the TypeScript sibling where mocked Sequelize tests passed while real queries failed), integration tests only (TestContainers + PostgreSQL adds setup cost and CI image pull time), and a mixed strategy with an explicit boundary. + +## Decision + +We will use MockMvc + Mockito for controller and service unit tests, including 500-level error branches that cannot be reached with a healthy database. We will use in-memory SQLite (`:memory:`) for integration tests that exercise the full request/response cycle for all reachable paths. The boundary is explicit: if a scenario can be triggered with a real database and real data, it must use the real database. + +## Consequences + +- In-memory SQLite gives near-zero test infrastructure cost: no Docker, no external process, no cleanup. Schema is applied via `ddl.sql` and seed data via `dml.sql` before each test class. +- Mockito covers error branches (e.g. repository throwing an exception) that cannot be triggered otherwise without test-environment side effects. +- The test naming convention (`givenX_whenY_thenZ`) and AssertJ BDD style (`then(result).isNotNull()`) make the intent of each test clear. +- The mixed strategy avoids the false confidence documented in the TypeScript sibling project, where mocked ORM tests passed while the real query implementation was broken. +- In-memory SQLite auto-clears between test classes; no `@Transactional` rollback or manual truncation is required. +- The boundary between mock and real-database tests must be maintained by convention. A test that mocks the database for a scenario reachable with real data is a latent gap in coverage. diff --git a/docs/adr/0012-adopt-flyway-migrations.md b/docs/adr/0012-adopt-flyway-migrations.md new file mode 100644 index 0000000..fa071ec --- /dev/null +++ b/docs/adr/0012-adopt-flyway-migrations.md @@ -0,0 +1,24 @@ +# ADR-0012: Adopt Flyway for Database Migrations + +Date: 2026-06-07 + +## Status + +Accepted + +## Context + +The project initially used manual `ddl.sql` and `dml.sql` scripts executed at startup for schema creation and seed data. This was sufficient for a PoC with a stable schema and a single committed SQLite file, but it provides no migration history, no versioning, and no safe path to evolve the schema over time. When PostgreSQL support was introduced (issue #286), versioned schema migrations became necessary. Options considered: Flyway (versioned, reversible migrations, first-class Spring Boot auto-configuration, Apache 2.0 license), Liquibase (XML/YAML/SQL changelogs, broader database support, more complex configuration), and retaining manual scripts (no migration history, not viable with multiple databases). Implemented in v2.0.1-chelsea (issue #130). + +## Decision + +We will adopt Flyway for versioned schema migrations, superseding the manual `ddl.sql`/`dml.sql` approach. Migration scripts are stored under `src/main/resources/db/migration/` following Flyway's `V{version}__{description}.sql` naming convention. Spring Boot's Flyway auto-configuration applies migrations at startup. + +## Consequences + +- Flyway provides a full migration history: every schema change is a versioned, auditable SQL file. Rollback requires a new migration rather than an undo, which is the correct model for production systems. +- Spring Boot's auto-configuration wires Flyway automatically when the dependency is present; no explicit bean definition is required. +- The migration from manual scripts to Flyway was a one-time conversion: the original `ddl.sql` and `dml.sql` content became V1 and V2 (or V3) Flyway scripts, preserving the 26-player seed dataset. +- Flyway enforces a checksum on applied migrations; modifying an already-applied migration file causes a startup failure. This is a safety mechanism, not a limitation. +- The in-memory SQLite test database also runs Flyway migrations, ensuring the test schema stays in sync with the production schema automatically. +- Flyway's Apache 2.0 license is compatible with the project's MIT license. The Flyway Teams/Enterprise editions are not required for this use case. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..a34491f --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,28 @@ +# Architecture Decision Records (ADR) + +This directory contains records of architecturally significant decisions made in this project. + +An ADR documents a single decision: the context that drove it, the alternatives considered, and the consequences of the choice. Records are immutable once accepted — superseded decisions get a new ADR rather than an edit. + +## 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 | Accepted | +| [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-adopt-flyway-migrations.md) | Adopt Flyway for Database Migrations | Accepted | + +## Resources + +- [Documenting Architecture Decisions — Michael Nygard (2011)](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) diff --git a/docs/adr/template.md b/docs/adr/template.md new file mode 100644 index 0000000..b5c82c2 --- /dev/null +++ b/docs/adr/template.md @@ -0,0 +1,24 @@ +# ADR-NNNN: {Title} + +Date: YYYY-MM-DD + +## Status + +{Proposed | Accepted | Deprecated | Superseded by ADR-XXXX} + +## Context + +{Describe the forces at play: technical, business, and team constraints. +What problem needs solving? What alternatives were considered?} + +## Decision + +{State the decision in full sentences using active voice: "We will..."} + +## Consequences + +{Describe the resulting context after applying this decision:} + +- {Positive outcome} +- {Negative trade-off} +- {Neutral implication} From b528b8b669783963e0fbcb152d1f8b371c1eb754 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Mon, 8 Jun 2026 10:49:14 -0300 Subject: [PATCH 2/3] fix(adr): address CodeRabbit review feedback on PR #338 Co-authored-by: Claude Sonnet 4.6 --- docs/adr/0008-stadium-themed-versioning.md | 2 +- docs/adr/0009-uuid-surrogate-squad-number-natural-key.md | 8 ++++---- docs/adr/0012-adopt-flyway-migrations.md | 2 +- docs/adr/README.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/adr/0008-stadium-themed-versioning.md b/docs/adr/0008-stadium-themed-versioning.md index c10930b..a1107be 100644 --- a/docs/adr/0008-stadium-themed-versioning.md +++ b/docs/adr/0008-stadium-themed-versioning.md @@ -1,4 +1,4 @@ -# ADR-0008: Use Stadium-Themed Versioning +# ADR-0008: Use Football-Club-Themed Versioning Date: 2026-06-07 diff --git a/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md b/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md index 8a246dd..be35880 100644 --- a/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md +++ b/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md @@ -1,4 +1,4 @@ -# ADR-0009: Demote UUID to Surrogate Key and Promote Squad Number to Natural Key +# ADR-0009: Use UUID as Primary Key and Squad Number as Natural Key Date: 2026-06-07 @@ -12,13 +12,13 @@ The `Player` entity originally used a sequential `Long` as its primary key — p ## Decision -We will demote UUID to a non-primary surrogate key — unique, non-updatable, generated at the application level via `@PrePersist`. `squadNumber` becomes the `@Id` (primary key) and is used as the path variable for `PUT /players/{squadNumber}` and `DELETE /players/{squadNumber}`. A `GET /players/{id}` endpoint (UUID) is retained for internal/admin lookup. +We will use UUID as the database primary key (`@Id`) — unique, non-updatable, generated at the application level via `@GeneratedValue(strategy = GenerationType.UUID)`. `squadNumber` becomes the natural key exposed in REST endpoints and is used as the path variable for `PUT /players/{squadNumber}` and `DELETE /players/{squadNumber}`. `squadNumber` carries a unique constraint (`@Column(unique = true)`) at the database level. A `GET /players/{id}` endpoint (UUID) is retained for internal/admin lookup. ## Consequences - Squad numbers are natural keys: unique per team, stable over a career, and meaningful to API consumers. Using them as path variables makes URLs readable (`/players/10`) rather than opaque (`/players/550e8400-e29b-41d4-a716-446655440000`). -- UUID as a surrogate key provides global uniqueness for distributed or federated scenarios without exposing sequential counters that hint at dataset size. -- The `@PrePersist`-generated UUID is set once on creation and never updated, ensuring immutability of the surrogate. +- UUID as the primary key provides global uniqueness for distributed or federated scenarios without exposing sequential counters that hint at dataset size. +- The `@GeneratedValue`-assigned UUID is set once on creation and never updated, ensuring immutability of the primary key. - Retaining `GET /players/{id}` (UUID) preserves internal traceability and supports integration with external systems that may store the UUID. - Squad numbers are stable within a single tournament squad but may change across seasons. For this PoC (fixed 2022 World Cup squad), stability is guaranteed. A more general player registry would need to account for squad number reuse. - The `squadNumber` field carries a `@Column(unique = true)` constraint enforced at the database level, providing a 409 Conflict response on duplicate POST. diff --git a/docs/adr/0012-adopt-flyway-migrations.md b/docs/adr/0012-adopt-flyway-migrations.md index fa071ec..40aa46b 100644 --- a/docs/adr/0012-adopt-flyway-migrations.md +++ b/docs/adr/0012-adopt-flyway-migrations.md @@ -20,5 +20,5 @@ We will adopt Flyway for versioned schema migrations, superseding the manual `dd - Spring Boot's auto-configuration wires Flyway automatically when the dependency is present; no explicit bean definition is required. - The migration from manual scripts to Flyway was a one-time conversion: the original `ddl.sql` and `dml.sql` content became V1 and V2 (or V3) Flyway scripts, preserving the 26-player seed dataset. - Flyway enforces a checksum on applied migrations; modifying an already-applied migration file causes a startup failure. This is a safety mechanism, not a limitation. -- The in-memory SQLite test database also runs Flyway migrations, ensuring the test schema stays in sync with the production schema automatically. +- Flyway is disabled for the in-memory SQLite test database (`spring.flyway.enabled=false`); tests continue to use `ddl.sql` and `dml.sql` via Spring SQL init for fast, dependency-free schema setup. - Flyway's Apache 2.0 license is compatible with the project's MIT license. The Flyway Teams/Enterprise editions are not required for this use case. diff --git a/docs/adr/README.md b/docs/adr/README.md index a34491f..cb4add4 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -15,8 +15,8 @@ An ADR documents a single decision: the context that drove it, the alternatives | [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 | Accepted | +| [ADR-0008](0008-stadium-themed-versioning.md) | Use Football-Club-Themed Versioning | Accepted | +| [ADR-0009](0009-uuid-surrogate-squad-number-natural-key.md) | Use UUID as Primary Key and Squad Number as Natural Key | Accepted | | [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-adopt-flyway-migrations.md) | Adopt Flyway for Database Migrations | Accepted | From f901e80715cff02052eabb6b45f43ce8eb88bcb9 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Mon, 8 Jun 2026 11:44:44 -0300 Subject: [PATCH 3/3] fix(adr): refine key terminology in ADR-0009 and add naming hint to template Co-authored-by: Claude Sonnet 4.6 --- .../0009-uuid-surrogate-squad-number-natural-key.md | 12 ++++++------ docs/adr/template.md | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md b/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md index be35880..deeadf2 100644 --- a/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md +++ b/docs/adr/0009-uuid-surrogate-squad-number-natural-key.md @@ -16,9 +16,9 @@ We will use UUID as the database primary key (`@Id`) — unique, non-updatable, ## Consequences -- Squad numbers are natural keys: unique per team, stable over a career, and meaningful to API consumers. Using them as path variables makes URLs readable (`/players/10`) rather than opaque (`/players/550e8400-e29b-41d4-a716-446655440000`). -- UUID as the primary key provides global uniqueness for distributed or federated scenarios without exposing sequential counters that hint at dataset size. -- The `@GeneratedValue`-assigned UUID is set once on creation and never updated, ensuring immutability of the primary key. -- Retaining `GET /players/{id}` (UUID) preserves internal traceability and supports integration with external systems that may store the UUID. -- Squad numbers are stable within a single tournament squad but may change across seasons. For this PoC (fixed 2022 World Cup squad), stability is guaranteed. A more general player registry would need to account for squad number reuse. -- The `squadNumber` field carries a `@Column(unique = true)` constraint enforced at the database level, providing a 409 Conflict response on duplicate POST. +- `squadNumber` is a domain-meaningful natural key: unique per team, stable over a career, and meaningful to API consumers. As the routing key in REST URLs (`PUT /players/{squadNumber}`, `DELETE /players/{squadNumber}`), it is more readable than an opaque UUID path. +- UUID is the entity primary key (`@Id`) and functions as a generated surrogate identifier — globally unique, opaque to consumers, and free of sequential leakage. It provides global uniqueness for distributed or federated scenarios without exposing sequential counters that hint at dataset size. +- The `@GeneratedValue`-assigned UUID (surrogate) is set once on creation and never updated, ensuring immutability of the primary key. +- `GET /players/{id}` exposes the UUID primary key for internal traceability and integration with external systems that store the UUID reference. +- `squadNumber` as routing key is stable within a fixed tournament squad but may change across seasons. For this PoC (fixed 2022 World Cup squad), stability is guaranteed. A more general player registry would need to account for squad number reuse. +- The `squadNumber` natural key carries a `@Column(unique = true)` constraint enforced at the database level, providing a 409 Conflict response on duplicate POST. diff --git a/docs/adr/template.md b/docs/adr/template.md index b5c82c2..f472e07 100644 --- a/docs/adr/template.md +++ b/docs/adr/template.md @@ -1,3 +1,4 @@ + # ADR-NNNN: {Title} Date: YYYY-MM-DD