Files
vmall/docs/adr/0001-rust-api-modular-monolith.md
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

2.6 KiB
Raw Permalink Blame History

0001. Modular monolith with handler / service / repository

  • Status: Accepted
  • Date: 2026-09-18
  • Deciders: VMall maintainers
  • Related: 0002, tech spec

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.