Skip to content

Commit 0810c52

Browse files
authored
Merge pull request #535 from nanotaboada/docs/architecture-decision-records
docs(adr): initialize Architecture Decision Records
2 parents 5610bf1 + f775727 commit 0810c52

12 files changed

+534
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ This project uses famous football coaches as release codenames, following an A-Z
4242

4343
## [Unreleased]
4444

45+
### Added
46+
47+
- Architecture Decision Records (ADRs) in `docs/adr/` documenting key
48+
decisions: SQLite, no Alembic, UUID primary key, squad number as
49+
mutation key, full replace PUT, in-memory caching, integration-only
50+
tests (#482)
51+
4552
---
4653

4754
## [2.0.0 - Capello] - 2026-03-17

CONTRIBUTING.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,41 @@ We value **incremental, detail‑first contributions** over big rewrites or abst
3737
- Run `pytest` before pushing.
3838
- Ensure coverage isn’t regressing.
3939

40-
## 3. Pull Request Workflow
40+
## 3. Architecture Decision Records
41+
42+
Significant architectural decisions are documented as ADRs in
43+
`docs/adr/`. Before changing or replacing a decision captured in an
44+
ADR, write a new ADR that supersedes it — do not edit the original.
45+
46+
**When to write an ADR:** apply the three-part test:
47+
1. A real fork existed — a genuine alternative was considered and
48+
rejected.
49+
2. The code doesn't explain the why — a new contributor reading the
50+
source cannot infer the reasoning.
51+
3. Revisiting it would be costly — changing it requires significant
52+
rework.
53+
54+
All three must be true. Process conventions (commit format, branch
55+
naming) and project principles (audience, tone) do not belong in ADRs.
56+
57+
ADRs are append-only. To change a decision: write a new ADR with
58+
status `SUPERSEDED by ADR-NNNN` on the old one.
59+
60+
## 4. Pull Request Workflow
4161

4262
- **One logical change per PR.**
4363
- **Rebase or squash** before opening to keep history clean.
4464
- **Title & Description**
4565
- Use Conventional Commit format.
4666
- Explain _what_ and _why_ concisely in the PR body.
4767

48-
## 4. Issue Reporting
68+
## 5. Issue Reporting
4969

5070
- Search open issues before creating a new one.
5171
- Include clear steps to reproduce and environment details.
5272
- Prefer **focused** issues—don’t bundle multiple topics.
5373

54-
## 5. Automation & Checks
74+
## 6. Automation & Checks
5575

5676
All PRs and pushes go through CI:
5777

@@ -62,7 +82,7 @@ All PRs and pushes go through CI:
6282

6383
PRs must pass all checks to be reviewed.
6484

65-
## 6. Code of Conduct & Support
85+
## 7. Code of Conduct & Support
6686

6787
- See `CODE_OF_CONDUCT.md` for guidelines and reporting.
6888
- For questions or planning, open an issue and use the `discussion` label, or mention a maintainer.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Proof of Concept for a RESTful API built with [Python 3](https://www.python.org/
2929
- [Releases](#releases)
3030
- [Environment Variables](#environment-variables)
3131
- [Command Summary](#command-summary)
32+
- [Architecture Decisions](#architecture-decisions)
3233
- [Contributing](#contributing)
3334
- [Legal](#legal)
3435

@@ -404,6 +405,11 @@ PYTHONUNBUFFERED=1
404405
| `docker compose down` | Stop Docker container |
405406
| `docker compose down -v` | Stop and remove Docker volume |
406407

408+
## Architecture Decisions
409+
410+
Key architectural decisions are documented as ADRs in
411+
[`docs/adr/`](docs/adr/README.md).
412+
407413
## Contributing
408414

409415
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct and the process for submitting pull requests.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# ADR-0001: SQLite as Database Engine
2+
3+
Date: 2026-03-21
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
This project is a proof-of-concept REST API. A database engine was
12+
required to persist player data. The realistic alternatives were:
13+
14+
- **PostgreSQL** — the standard choice for production REST APIs; requires
15+
a running server process, a connection string, and either a local
16+
installation or a Docker service alongside the application.
17+
- **MySQL / MariaDB** — similar trade-offs to PostgreSQL.
18+
- **SQLite** — an embedded, file-based engine; no server process, no
19+
installation beyond a driver, and the database file can be committed
20+
to the repository pre-seeded.
21+
22+
The project includes a pre-seeded database file (`storage/players-sqlite3.db`)
23+
that contains all 26 players from the Argentina 2022 World Cup squad.
24+
SQLAlchemy 2.0 abstracts most engine-specific SQL, and aiosqlite provides
25+
an async driver compatible with the rest of the async stack.
26+
27+
## Decision
28+
29+
We will use SQLite as the database engine via `aiosqlite` and
30+
SQLAlchemy 2.0 (async).
31+
32+
The deciding factor is operational simplicity for a PoC: zero server
33+
infrastructure, a self-contained database file that can be seeded once
34+
and committed, and a one-command startup (`uv run uvicorn main:app`).
35+
The async driver (`aiosqlite`) keeps the stack consistent with the
36+
rest of the async I/O patterns, and SQLAlchemy abstracts enough of the
37+
engine differences that migrating to PostgreSQL later would require
38+
minimal changes to the application code.
39+
40+
## Consequences
41+
42+
**Positive:**
43+
- No external service dependency — the application runs with a single
44+
command and no Docker Compose required for development.
45+
- The pre-seeded database file ships with the repository, making the
46+
project immediately runnable with real data.
47+
- SQLAlchemy 2.0 abstracts most engine-specific behavior; a future
48+
migration to PostgreSQL is feasible with driver and connection string
49+
changes only.
50+
51+
**Negative:**
52+
- SQLite does not support concurrent writes. This is acceptable for a
53+
single-instance PoC but rules out horizontal scaling without a
54+
database change.
55+
- Some PostgreSQL features (e.g. `RETURNING`, advisory locks, full-text
56+
search) are unavailable or behave differently.
57+
- The committed database file can accumulate stale data if seed scripts
58+
are updated but the file is not regenerated manually.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# ADR-0002: No Alembic — Manual Seed Scripts
2+
3+
Date: 2026-03-21
4+
5+
## Status
6+
7+
Accepted. Migration to Alembic is under consideration — tracked in
8+
issue #2.
9+
10+
## Context
11+
12+
SQLAlchemy projects typically use Alembic to manage schema migrations:
13+
versioned migration scripts, upgrade/downgrade paths, and a migration
14+
history table. The alternatives considered were:
15+
16+
- **Alembic** — the standard SQLAlchemy migration tool; provides
17+
versioned, reversible migrations and is well-supported in production.
18+
- **Manual seed scripts** — standalone Python scripts in `tools/` that
19+
create the schema and populate data directly using SQLAlchemy models;
20+
no migration history, no upgrade/downgrade concept.
21+
- **No seeding** — start from an empty database and rely on the API to
22+
create all data; unsuitable since the project ships a fixed dataset
23+
(Argentina 2022 squad).
24+
25+
The project uses a fixed, pre-seeded SQLite database file
26+
(`storage/players-sqlite3.db`) committed to the repository. Schema
27+
evolution has been infrequent and handled by regenerating the file
28+
manually from updated seed scripts.
29+
30+
## Decision
31+
32+
We will use standalone seed scripts in `tools/` instead of Alembic.
33+
34+
For a PoC with a stable schema and a single committed database file,
35+
Alembic adds tooling overhead (initialization, migration directory,
36+
`env.py` configuration) without meaningful benefit. The seed scripts
37+
(`tools/seed_001_starting_eleven.py`, `tools/seed_002_substitutes.py`)
38+
are self-contained and produce a deterministic output using UUID v5
39+
for stable, reproducible primary keys.
40+
41+
## Consequences
42+
43+
**Positive:**
44+
- No Alembic dependency or configuration; simpler project setup.
45+
- Seed scripts are plain Python, easy to read and modify.
46+
- Deterministic UUID v5 keys mean the seeded database is identical
47+
across environments and safe to reference in tests.
48+
49+
**Negative:**
50+
- No migration history. Schema changes require manually updating the
51+
database file and rerunning seed scripts; there is no upgrade path.
52+
- Not suitable for a production deployment where data must be preserved
53+
across schema changes.
54+
- As schema complexity grows, the absence of migration tooling becomes
55+
increasingly costly. Issue #2 tracks the future adoption of Alembic.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ADR-0003: UUID Surrogate Primary Key with v4/v5 Split
2+
3+
Date: 2026-03-21
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
A primary key strategy was required for the `players` table. The
12+
candidates considered were:
13+
14+
- **Auto-increment integer** — simple, compact, sequential; but leaks
15+
row count and insertion order to clients, and integer IDs are
16+
predictable, which can be a security concern for resource enumeration.
17+
- **UUID v4** — randomly generated, opaque, non-sequential; no
18+
information leaked; standard for API-created resources.
19+
- **UUID v7 / ULID** — time-ordered UUIDs; better index locality than
20+
v4 but add a dependency and are less universally supported.
21+
- **Natural key (`squad_number`)** — human-readable, but not stable
22+
across squads or seasons; unsuitable as a surrogate.
23+
24+
An additional constraint was seeded data: the project ships 26
25+
pre-seeded players that must have stable, reproducible primary keys
26+
across environments so that tests can reference them by ID.
27+
28+
UUID v4 (random) cannot satisfy this: regenerating the seed would
29+
produce different IDs each time. UUID v5 (deterministic, derived from
30+
a namespace and a name) produces the same UUID for the same input,
31+
making seeded records reproducible. The project stores pre-computed
32+
UUID v5 constants directly in the seed scripts rather than deriving
33+
them at runtime from the squad number.
34+
35+
## Decision
36+
37+
We will use a UUID surrogate primary key stored as a hyphenated string
38+
(`HyphenatedUUID` custom SQLAlchemy type). API-created records receive
39+
a UUID v4 (random); migration-seeded records receive a UUID v5
40+
(deterministic, pre-computed and stored as constants in the seed
41+
scripts).
42+
43+
The v4/v5 split preserves the benefits of each approach in its context:
44+
randomness for API-created records (opaque, non-predictable), and
45+
determinism for seeded records (stable across environments, safe for
46+
test fixtures).
47+
48+
The UUID is intentionally opaque to external clients. It is exposed
49+
only via `GET /players/{player_id}` and `POST /players/` responses.
50+
All mutation endpoints (PUT, DELETE) use `squad_number` as the
51+
identifier — see ADR-0004.
52+
53+
## Consequences
54+
55+
**Positive:**
56+
- UUIDs are opaque; no information about row count or insertion order
57+
is exposed to clients.
58+
- The v5/v4 split means seeded records have stable IDs across
59+
environments, safe to hard-code in tests.
60+
- `HyphenatedUUID` stores IDs as standard hyphenated strings in SQLite,
61+
readable without special tooling.
62+
63+
**Negative:**
64+
- UUIDs are larger than integers (36 chars as strings), which has a
65+
minor storage and index performance cost — acceptable at PoC scale.
66+
- The v4/v5 distinction is non-obvious; developers unfamiliar with the
67+
codebase may not know why seeded records have deterministic IDs.
68+
- Clients cannot use the UUID to infer insertion order or paginate
69+
reliably by ID (UUID v4 is random).
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# ADR-0004: Squad Number as the Mutation Key
2+
3+
Date: 2026-03-21
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The API exposes two identifiers for a player:
12+
13+
- **UUID** (`id`) — surrogate key; opaque, server-generated, stable.
14+
See ADR-0003.
15+
- **Squad number** (`squad_number`) — natural key; human-readable,
16+
domain-meaningful (e.g. number 10 = Messi), unique within a squad.
17+
18+
A design choice was required for which identifier mutation endpoints
19+
(PUT, DELETE) and secondary GET endpoints should use as their path
20+
parameter. The candidates were:
21+
22+
- **UUID for all endpoints** — consistent use of the surrogate key;
23+
clients must store UUIDs after POST or GET to perform mutations.
24+
- **Squad number for mutations** — uses the natural, human-meaningful
25+
key for the operations that domain users care about; UUID remains
26+
available for internal/service-to-service use via
27+
`GET /players/{player_id}`.
28+
29+
Up to v1.x, the API used UUID for PUT and DELETE. Version 2.0.0
30+
(Capello) changed mutation endpoints to use squad number, introduced
31+
`GET /players/squadnumber/{squad_number}` as a lookup alternative, and
32+
retained `GET /players/{player_id}` for UUID-based lookup.
33+
34+
## Decision
35+
36+
We will use `squad_number` as the path parameter for all mutation
37+
endpoints (`PUT /players/squadnumber/{squad_number}`,
38+
`DELETE /players/squadnumber/{squad_number}`) and for the secondary
39+
GET endpoint (`GET /players/squadnumber/{squad_number}`).
40+
41+
Squad number is the identifier that domain users reason about. Requiring
42+
clients to store and re-use a UUID to update or delete a player they
43+
identified by squad number adds unnecessary indirection. Keeping UUID
44+
lookup available (`GET /players/{player_id}`) preserves
45+
service-to-service use cases where a stable opaque key is preferred.
46+
47+
As a consequence of using squad number on PUT, the request body's
48+
`squad_number` field must match the path parameter. A mismatch returns
49+
HTTP 400; the path parameter is always authoritative.
50+
51+
## Consequences
52+
53+
**Positive:**
54+
- Mutation endpoints are intuitive for domain users:
55+
`PUT /players/squadnumber/10` clearly targets Messi's record.
56+
- Clients do not need to perform a GET to obtain a UUID before mutating.
57+
- UUID remains available for stable, opaque internal references.
58+
59+
**Negative:**
60+
- Squad numbers can change (a player re-assigned to a different number
61+
requires a careful update sequence). The API has no special handling
62+
for squad number changes — it is treated as any other field update.
63+
- Two lookup endpoints (`/players/{id}` and
64+
`/players/squadnumber/{squad_number}`) increase surface area slightly.
65+
- The mismatch guard on PUT (body squad number must equal path
66+
parameter) is an additional contract constraint clients must respect.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# ADR-0005: Full Replace PUT, No PATCH
2+
3+
Date: 2026-03-21
4+
5+
## Status
6+
7+
Accepted. Partial update support via PATCH is under consideration —
8+
tracked in issue #461.
9+
10+
## Context
11+
12+
HTTP defines two semantics for updating a resource:
13+
14+
- **PUT** — full replacement; the request body represents the complete
15+
new state of the resource. Fields absent from the body are
16+
overwritten with their zero/null values.
17+
- **PATCH** — partial update; the request body contains only the fields
18+
to change. Requires a patch format (JSON Merge Patch, JSON Patch) or
19+
a convention for interpreting absent fields.
20+
21+
Both are common in REST APIs. The choice affects the Pydantic model
22+
design (all fields required vs optional), the service layer logic
23+
(overwrite all vs merge), and the client contract (must send all fields
24+
vs only changed fields).
25+
26+
The current implementation uses a single `PlayerRequestModel` shared by
27+
both POST and PUT. Four fields are required (`first_name`, `last_name`,
28+
`squad_number`, `position`); the remaining six are optional (`middle_name`,
29+
`date_of_birth`, `abbr_position`, `team`, `league`, `starting11`). The
30+
service's `update_by_squad_number_async` overwrites every field on the
31+
existing record using the request body values.
32+
33+
## Decision
34+
35+
We will implement PUT as a full replacement operation only, with no
36+
PATCH endpoint.
37+
38+
Full replace is simpler to implement correctly: no merge logic, no
39+
ambiguity about what an absent field means, and a single request model
40+
covers both POST and PUT. For a PoC with a small, flat domain model
41+
(10 fields), requiring the full resource on update is not a meaningful
42+
burden for clients.
43+
44+
## Consequences
45+
46+
**Positive:**
47+
- Simple, unambiguous semantics: the request body is the new state.
48+
- No additional Pydantic model or merge logic required.
49+
- No need to decide on a patch format (JSON Merge Patch vs JSON Patch).
50+
51+
**Negative:**
52+
- Clients must send the full resource even to change a single field,
53+
which is verbose and risks accidental data loss if a field is omitted.
54+
- Not suitable for resources with many fields, binary data, or
55+
frequently partial updates.
56+
- Issue #461 tracks the addition of PATCH for partial updates, which
57+
would require an optional-fields model and merge logic in the service
58+
layer.

0 commit comments

Comments
 (0)