docs: API architecture ADRs, tech spec, and marketing capability tracker
Persist the modular-monolith decision (ADR 0001 handler/service/repo, ADR 0002 keep REST) with the companion tech spec and the api-architecture OpenSpec capability. Replace the finished mock-migration tracker with docs/TBD-marketing.md listing the backend-less marketing domains still on fixtures (coupons, favorites, account stats, seckill, collective, integral, reviews). README and AGENTS.md point at the new docs.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# 0001. Modular monolith with handler / service / repository
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-09-18
|
||||
- Deciders: VMall maintainers
|
||||
- Related: [0002](0002-keep-http-rest-not-graphql.md), [tech spec](../tech-specs/rust-api.md)
|
||||
|
||||
## Context
|
||||
|
||||
`vmall-api` started as Axum route modules with SQL, validation, and HTTP mapping in the same handler. That was fine for an MVP of a few files. Checkout, stock, shipment status, and invoices then lived in 200–400 line handlers, duplicated across customer / shop / admin surfaces, with `SELECT *` leaking `password_hash` behind `skip_serializing`.
|
||||
|
||||
Rails-style MVC does not map cleanly onto Axum: there is no View layer, and “Controller” is just the handler. Full hexagonal / DDD (ports, adapters, domain events) would add compile time and indirection without a second persistence backend.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep a **single crate** (`vmall-api`) as a **modular monolith**. Split code by **bounded context** under `apps/api/src/modules/<context>/`, with three roles:
|
||||
|
||||
| Layer | Owns | Must not own |
|
||||
|-------|------|----------------|
|
||||
| Handler | Axum extracts, RBAC, HTTP status, JSON envelope | SQL, Redis, state machines |
|
||||
| Service | Use cases (checkout, default address, publish product) | `Json`, `StatusCode`, path params |
|
||||
| Repository / store | sqlx and Redis | HTTP types |
|
||||
|
||||
Simple CRUD may skip the service file and call the repository from the handler. Do **not** introduce a generic `Repository` trait unless a second backend exists.
|
||||
|
||||
Shared crate roots stay small: `error`, `auth`, `models`, `money`, `state`, `http` (pagination / query DTOs), `config`, `seed`.
|
||||
|
||||
HTTP paths, JSON field names, and `{"error":{"code","message"}}` stay unchanged so `@vmall/shared` and the three Nuxt apps do not move.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- Customer, shop, and admin order lists share `order::service` with an `OrderScope`.
|
||||
- Checkout and fulfillment can be unit-tested against `AppState` without HTTP.
|
||||
- New features land in an existing module instead of growing `routes/*.rs`.
|
||||
|
||||
Negative:
|
||||
|
||||
- More files per use case; trivial list endpoints look heavier than a single handler.
|
||||
- Cross-module calls (fulfillment → order repo) must stay explicit; no hidden event bus.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep fat handlers.** Rejected: checkout and shipment transitions were already hard to reuse.
|
||||
|
||||
**Classic MVC packages (`controllers/`, `services/`, `models/`).** Rejected: splits a use case across three top-level trees; Axum has no views.
|
||||
|
||||
**Hexagonal architecture + domain events.** Rejected for current size: one Postgres, one Redis, one process.
|
||||
|
||||
**Framework switch (Loco, Actix).** Rejected: Axum 0.8 already matches the stack; a rewrite would not fix layering.
|
||||
Reference in New Issue
Block a user