Worked example — an HTTP CRUD service under SAMA v2

The /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:

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.

// 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<OrderLine>;
  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<void>;
  findById(id: string): Promise<Order | null>;
  listByCustomer(customerId: string): Promise<Order[]>;
}

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).

// 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<Order> => {
  // ... validates, computes totals via pure a_pricing, calls deps.orders.save
};

export const cancelOrder = async (deps: OrderServiceDeps, orderId: string): Promise<void> => {
  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.

// 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.

// 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<Response> => {
  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