Files
vmall/docs/adr/0001-rust-api-modular-monolith.md
T
Chengdong Zhang e10cae5789 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.
2026-09-18 16:00:31 +08:00

52 lines
2.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 200400 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.