# 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//`, 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.