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.
2.6 KiB
0001. Modular monolith with handler / service / repository
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::servicewith anOrderScope. - Checkout and fulfillment can be unit-tested against
AppStatewithout 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.