# Worked example — an HTTP CRUD service under SAMA v2 The [/sama/v2](/sama/v2) spec is profile-agnostic by design. This page is one concrete instantiation of it: a small e-commerce orders API. The same four canonical layers and the same import law govern; only the **profile** changes — the sublayer split inside Layers 1 and 2 reflects what HTTP-CRUD services actually need. Use this as a reading anchor, not a copy-paste skeleton. A SAMA v2-conformant CRUD service has many valid shapes; this page commits to one and explains every decision. ## The profile A single `sama.profile.toml` at the repo root, machine-ingestible by the verifier: ```toml sama_version = "2.0" profile = "orders-api" # Layer 0 — Pure. Domain types, validation rules as data, pure transforms. # No I/O, no clock, no framework. [layers.0] prefixes = ["a_"] # Layer 1 — Core. Business decisions and orchestration. No DB, no HTTP, # no clock. Calls into Layer 2 only through dependency-injected interfaces # the Pure layer declares. # - b1_ policy: pure decisions over domain state ("canCancelOrder") # - b2_ service: orchestrators that compose policy + delegate to adapters # (b2 may import b1; b1 must never import b2) [layers.1] sublayers = [ { name = "policy", prefix = "b1_" }, { name = "service", prefix = "b2_" }, ] # Layer 2 — Adapter. The boundary. External input is PARSED here (never # cast). DB, network, filesystem, framework bindings. # - c1_ repository: DB-backed CRUD (one repo per aggregate) # - c2_ gateway: outbound HTTP / SMTP / SMS to third parties # - c3_ controller: inbound HTTP request parsing (JSON.parse, new URL, # schema validation). NOT to be confused with Layer 3 route handlers. # c3 turns bytes into typed structures; d_ wires those structures to # a service call. [layers.2] sublayers = [ { name = "repository", prefix = "c1_" }, { name = "gateway", prefix = "c2_" }, { name = "controller", prefix = "c3_" }, ] # Layer 3 — Entry. Route handlers + server bootstrap. Composes c3_ # controllers + b2_ services. Owns no business logic. [layers.3] prefixes = ["d_"] ``` Lex check: `a_ < b1_ < b2_ < c1_ < c2_ < c3_ < d_`. Layer order: `0 < 1 < 1 < 2 < 2 < 2 < 3`. The Sorted check (§4.1) is satisfied for free — `ls src/` reads top-to-bottom in dependency order. ## The directory ``` src/ ├── a_order.ts # Layer 0 — Order, OrderLine, OrderStatus types ├── a_customer.ts # Layer 0 — Customer, Address types ├── a_payment.ts # Layer 0 — Payment, Money, Currency types ├── a_pricing.ts # Layer 0 — pure pricing math, tax rules ├── a_ports.ts # Layer 0 — adapter interface declarations │ # (OrderRepository, PaymentGateway, ...) ├── b1_order_policy.ts # Layer 1 — canCancelOrder, isEligibleForRefund ├── b1_payment_policy.ts # Layer 1 — paymentRetryStrategy, refundWindow ├── b2_order_service.ts # Layer 1 — createOrder, fulfillOrder, cancelOrder ├── b2_payment_service.ts # Layer 1 — chargeOrder, refundOrder ├── c1_order_repo.ts # Layer 2 — implements OrderRepository against bun:sqlite ├── c1_customer_repo.ts # Layer 2 — implements CustomerRepository ├── c2_stripe_gateway.ts # Layer 2 — implements PaymentGateway via Stripe REST ├── c2_email_gateway.ts # Layer 2 — implements EmailGateway via Resend ├── c3_order_controller.ts # Layer 2 — parseCreateOrderRequest, parseListQuery ├── c3_payment_controller.ts # Layer 2 — parseChargeRequest, parseRefundRequest ├── d_orders_handlers.ts # Layer 3 — POST /orders, GET /orders/:id, ... ├── d_payments_handlers.ts # Layer 3 — POST /payments, POST /refunds └── d_server.ts # Layer 3 — Bun.serve / Express / Hono bootstrap + sibling .test.ts for every b*_ and c*_ file (Modeled-tests check §4.3) ``` Twenty-odd source files plus sibling tests. Small enough to navigate; covers every layer the spec names. ## Layer 0 — Pure Domain types, validation rules as data, pure transforms. No I/O. No clock. No `new Date()` inside a Layer 0 function (use a clock value passed in from Layer 1+). No framework imports — `node:fs`, `express`, `bun:sqlite`, `Bun.serve` all live higher. ```ts // a_order.ts export type OrderStatus = "pending" | "paid" | "fulfilled" | "cancelled"; export interface OrderLine { productId: string; quantity: number; unitPriceCents: number; } export interface Order { id: string; customerId: string; lines: ReadonlyArray; status: OrderStatus; createdAt: string; // ISO 8601; produced by Layer 1+ } // a_ports.ts — the interfaces Layer 1 calls through. Implementations // live in Layer 2; declaring them in Layer 0 lets b2_*_service.ts // import only types (no upward edge to c1_*). export interface OrderRepository { save(order: Order): Promise; findById(id: string): Promise; listByCustomer(customerId: string): Promise; } export interface PaymentGateway { charge(orderId: string, amountCents: number, currency: string): Promise<{ chargeId: string } | { error: string }>; } ``` The crucial Layer 0 move: **adapter interfaces declared here, not in Layer 2**. A `b2_order_service.ts` that orchestrates `OrderRepository` doesn't import from `c1_order_repo.ts` — it imports the *interface* from `a_ports.ts` and is handed a concrete instance at construction. This is what keeps the Law check green: Layer 1 → Layer 0 only. ## Layer 1 — Core Business logic and orchestration. Two sublayers — `b1_` policy (pure decisions) and `b2_` service (composes policy + delegates to interfaces). `b2` may import `b1`; `b1` must never import `b2` (the profile's sublayer order enforces this via §2.2). ```ts // b1_order_policy.ts — pure functions, no I/O, no clock import type { Order } from "./a_order.ts"; export const canCancelOrder = (order: Order, nowIso: string): boolean => { if (order.status !== "pending" && order.status !== "paid") return false; const ageMs = Date.parse(nowIso) - Date.parse(order.createdAt); return ageMs < 24 * 60 * 60 * 1000; // 24-hour cancellation window }; // b2_order_service.ts — orchestrator. Takes ports at construction. import type { Order, OrderRepository, PaymentGateway } from "./a_ports.ts"; import { canCancelOrder } from "./b1_order_policy.ts"; export interface OrderServiceDeps { orders: OrderRepository; payments: PaymentGateway; now: () => string; // clock as a port — pure functions stay pure } export const createOrder = async (deps: OrderServiceDeps, input: CreateOrderInput): Promise => { // ... validates, computes totals via pure a_pricing, calls deps.orders.save }; export const cancelOrder = async (deps: OrderServiceDeps, orderId: string): Promise => { const order = await deps.orders.findById(orderId); if (!order) throw new Error("order not found"); if (!canCancelOrder(order, deps.now())) throw new Error("outside cancellation window"); await deps.orders.save({ ...order, status: "cancelled" }); }; ``` Every Layer 1 file has a sibling `.test.ts` (Modeled-tests check §4.3). The policy tests are pure unit tests over fixture orders; the service tests use in-memory fake implementations of the ports (not mocks — concrete in-memory `OrderRepository` and `PaymentGateway` that the test constructs). ## Layer 2 — Adapter The boundary. Three sublayers — `c1_` repository (DB), `c2_` gateway (outbound HTTP), `c3_` controller (inbound HTTP parsing). Within Layer 2, controllers may import repositories + gateways through their interfaces. ```ts // c1_order_repo.ts — implements a_ports.OrderRepository import { Database } from "bun:sqlite"; import type { Order, OrderRepository } from "./a_ports.ts"; export const createOrderRepository = (db: Database): OrderRepository => ({ save: async (order) => { db.prepare("INSERT OR REPLACE INTO orders (id, customer_id, status, ...) VALUES (?, ?, ?, ...)") .run(order.id, order.customerId, order.status); }, findById: async (id) => { const row = db.prepare("SELECT * FROM orders WHERE id = ?").get(id); return row ? rowToOrder(row) : null; }, // ... }); // c3_order_controller.ts — parsing only. The §4.4 Modeled-boundary // check FORBIDS JSON.parse / new URL outside this layer. import type { CreateOrderInput } from "./a_order.ts"; export const parseCreateOrderRequest = (body: string): { ok: true; value: CreateOrderInput } | { ok: false; error: string } => { let parsed: unknown; try { parsed = JSON.parse(body); // ← parse only here } catch (e) { return { ok: false, error: "invalid JSON" }; } // ... schema validation, returns typed CreateOrderInput }; ``` Layer 2 imports from Layer 0 and Layer 1 (e.g. a repository helper might call a `b1_` policy to enrich rows before returning). Never from Layer 3. ## Layer 3 — Entry Route handlers + server bootstrap. Owns zero business logic — every handler is a thin composition: parse request via `c3_*_controller`, invoke `b2_*_service`, serialize the result. ```ts // d_orders_handlers.ts import { parseCreateOrderRequest } from "./c3_order_controller.ts"; import { createOrder } from "./b2_order_service.ts"; export const postOrdersHandler = (deps: OrderServiceDeps) => async (req: Request): Promise => { const body = await req.text(); const parsed = parseCreateOrderRequest(body); // bytes → typed if (!parsed.ok) return new Response(parsed.error, { status: 400 }); const order = await createOrder(deps, parsed.value); // pure orchestration call return Response.json(order, { status: 201 }); }; // d_server.ts import { Database } from "bun:sqlite"; import { createOrderRepository } from "./c1_order_repo.ts"; import { createStripeGateway } from "./c2_stripe_gateway.ts"; import { postOrdersHandler } from "./d_orders_handlers.ts"; const db = new Database("orders.db"); const deps = { orders: createOrderRepository(db), payments: createStripeGateway(process.env.STRIPE_KEY!), now: () => new Date().toISOString(), }; Bun.serve({ port: 3000, routes: { "POST /orders": postOrdersHandler(deps) }, }); ``` The Entry layer is where adapters get wired into services. Test files don't normally sit beside `d_*` — the Modeled-tests rule (§4.3) only requires sibling tests for Layer 1 and Layer 2. Layer 3 is exercised by end-to-end integration tests, not unit tests. ## Common mistakes — and the check that catches them | What an agent might do | Which v2 check catches it | |---|---| | Put `JSON.parse(req.body)` inside `d_orders_handlers.ts` | **#4 Modeled (boundary)** — boundary parsing must live in Layer 2 (here: `c3_*_controller.ts`) | | Import `c1_order_repo.ts` from `b2_order_service.ts` | **#6 Law (§1.2)** — Layer 1 → Layer 2 is upward (use the `a_ports.ts` interface instead) | | Import `b1_order_policy.ts` from `b2_order_service.ts` | ✓ Legal — same layer, `b2` is later in the sublayer order so it may import `b1` | | Import `b2_order_service.ts` from `b1_order_policy.ts` | **#6 Law (§1.2)** — same layer but reversed sublayer order (`b1` may not import `b2`) | | Create `b2_payment_service.ts` without a sibling `.test.ts` | **#3 Modeled (tests)** — Layer 1 behaviour files require sibling tests | | Let `a_pricing.ts` import `b1_order_policy.ts` | **#7 Consistency (§3)** — file's `a_` prefix claims Pure but imports reach Layer 1; the prefix lies | | Let `d_orders_handlers.ts` grow to 850 LOC | **#5 Atomic** — over the 700-line cap; split per route group | | Add a `src/utils/json.ts` (no `a_`/`b_`/`c_`/`d_` prefix) | **#2 Architecture** — unprefixed file does not map to any canonical layer | Every column 2 entry is a deterministic check the verifier runs — the agent's harness cannot talk it out of the verdict. ## See also - [/sama/v2](/sama/v2) — the canonical spec this profile instantiates - [/sama/v2/example-wordpress](/sama/v2/example-wordpress) — the same shape against WordPress + PHP - [/sama/v2/verify](/sama/v2/verify) — the live verifier running against this site's own (different) profile - [/sama/v2#5-operational--core-metrics-definitions](/sama/v2#5-operational--core-metrics-definitions) — the §5 metrics this profile would emit [← /sama/v2](/sama/v2) · [← /sama](/sama)