One Request, One Complete System
Build: ParcelFlow accepts one tenant-scoped order and returns a delivery promise from one Bun process.
The enterprise problem and today’s slice
Enterprise problem: A merchant cannot promise delivery when an order disappears between validation, payment, and inventory, and a tenant leak would turn an operational mistake into a customer trust failure.
Whole-course context: This is the first slice of ParcelFlow, the recurring order-to-delivery system; the course starts with one complete process so later service boundaries must be earned by evidence rather than assumed.
Today’s slice: Build one POST /orders request with Bun.serve, explicit validation, an in-memory repository, and a fake payment adapter; real payment networks and durable storage remain outside the boundary.
End-of-day evidence: A red-then-green Bun test and one HTTP request show a valid tenant receives an order identifier while malformed input receives a stable rejection without creating state.
Still unsolved: A process restart loses every order, concurrent reservations can oversell inventory, and no production authentication, database, message broker, or service extraction exists yet.
The thesis is simple: begin with the smallest complete system that can prove a customer outcome, then preserve seams inside it. The smallest complete model is request, application decision, and observable response.
Customer use cases
When the customer job is vague, implementation can pass unit tests while still failing the merchant; use cases bind code to visible outcomes.
| Use case ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D01-UC-01 | Tenant checkout client | Submit a valid order for an available parcel item | HTTP 201 contains a tenant-scoped order ID and accepted status | A malformed item or missing tenant header returns HTTP 400 and no order is stored |
| D01-UC-02 | ParcelFlow developer | Prove one request crosses validation, payment, inventory, and persistence | A deterministic end-to-end test observes one stored order and one fake payment authorization | A declined fake payment returns HTTP 402 and inventory remains unchanged |
Actor-centred user stories
Without observable stories, a team can confuse internal function calls with delivered value and miss negative-path regressions.
| Story ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D01-US-01 | D01-UC-01 | As a tenant checkout client, I want to submit one valid order, so that I can show the buyer a delivery promise | Response is 201, body contains an order ID and accepted status, and a missing tenant or invalid quantity returns 400 with no stored record |
| D01-US-02 | D01-UC-02 | As a ParcelFlow developer, I want a tracer-bullet test through the whole process, so that future refactors preserve customer behaviour | The fake payment records one authorization, the repository contains one matching tenant order, and a configured decline produces 402 without a reservation |
End-to-end product flows
If a flow starts inside a service method, customer-triggered failures vanish from the design and the response contract becomes accidental.
| Flow ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D01-FLOW-01 | D01-UC-01, D01-UC-02 | Happy | Checkout sends POST /orders with tenant header and one valid line | 1. Parse JSON.<br>2. Validate tenant and quantity.<br>3. Ask fake payment to authorize.<br>4. Reserve in-memory stock.<br>5. Save order.<br>6. Return 201. | Response body, captured payment call, inventory decrement, and repository record share tenant and order IDs |
| D01-FLOW-02 | D01-UC-01, D01-UC-02 | Denied | Checkout omits tenant, sends invalid quantity, or uses the fake decline token | 1. Parse request.<br>2. Reject invalid identity or shape with 400, or reject decline with 402.<br>3. Do not reserve or save.<br>4. Read repository and fake-call log. | Stable error code plus an empty repository and unchanged stock provide negative evidence |
System design derived from the flows
When one process is treated as one undifferentiated module, changing HTTP parsing can silently alter payment or inventory behaviour.
| Use case ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D01-UC-01 | POST /orders on Bun.serve | HTTP adapter delegates to order application; fake payment and inventory repository are replaceable adapters | In-memory order map owned by the order module | HTTP 400/402 with no inserted order and no stock decrement |
| D01-UC-02 | bun test calling the same app fetch handler | Test client, order application, fake payment adapter, and in-memory repositories | Test-owned in-memory maps and captured adapter calls | Assertion shows mismatched response, call count, order state, or inventory state |
Data model and ownership
If ownership is postponed because storage is temporary, later persistence can encode the wrong invariants and leak one tenant’s order into another tenant’s query.
Generated-application database: Not created in this slice — the order module owns intentionally disposable in-memory records while the request contract is proved; Day 04 replaces them with durable PostgreSQL state.
| Record or entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| Order | In-memory Map owned by order module | order_id UUID | payment_authorization_id opaque fake-adapter reference | tenant_id from validated header | Quantity is positive; status starts accepted; all reads include tenant | Created after authorization and reservation; disappears on restart; test teardown clears it | D01-UC-01, D01-UC-02 |
| InventoryItem | In-memory Map owned by inventory adapter | sku | None — item is a root record in this temporary store | tenant_id is part of lookup key | Available count never becomes negative in this single-request model | Seeded per test, decremented on acceptance, reset at teardown or restart | D01-UC-01, D01-UC-02 |
Bun’s minimal runtime model
When a new runtime is presented as a catalogue of speed claims, developers miss the operational question: which executable owns request handling, tests, dependencies, and compatibility risk?
The general rule is to adopt the smallest runtime surface that proves the workload. In the simple ParcelFlow example, the bun executable runs TypeScript directly, Bun.serve exposes a Fetch-compatible handler, and bun test drives that same handler. In a realistic repository, the package manager and test runner may also be Bun while CI separately runs the TypeScript compiler. The failure mode is assuming “Node-compatible” means every package and edge behaviour is identical. The decision rule is: keep a compatibility test for each important dependency and choose Bun only when measured startup, throughput, or tool consolidation is worth that test burden.
Bun’s official documentation describes the runtime, package manager, bundler, and test runner as one toolkit, while also calling Node compatibility an ongoing effort. The server API accepts a fetch(Request) function, so the domain does not need to know about sockets or framework objects. See Bun overview, HTTP server, and Node.js compatibility.
TDD tracer bullet and request validation
If the first test exercises only a helper, the application can remain unassembled and the first real HTTP request can fail at the seams.
Test-driven development (TDD) means writing a failing behavioural test, making the smallest implementation pass, then improving structure without changing behaviour. A tracer bullet is a thin path through every essential layer. Start with the request-level test, not with repository classes:
import { expect, test } from "bun:test";
import { createApp } from "./app";
import { memoryInventory, memoryOrders } from "./memory";
test("accepts one tenant order end to end", async () => {
const orders = memoryOrders();
const inventory = memoryInventory({ "tenant-a:parcel-small": 3 });
const app = createApp({
orders,
inventory,
payment: { authorize: async () => ({ id: "pay_fake_01" }) },
});
const response = await app.fetch(new Request("http://local/orders", {
method: "POST",
headers: { "content-type": "application/json", "x-tenant-id": "tenant-a" },
body: JSON.stringify({ sku: "parcel-small", quantity: 1 }),
}));
expect(response.status).toBe(201);
expect(orders.allFor("tenant-a")).toHaveLength(1);
});
The simple validation rule rejects missing identity, unknown fields, invalid JSON, and non-positive quantity before side effects. A realistic request also has size limits, normalized SKU syntax, a request ID, and a content-type check. The failure mode is validating after payment or inventory mutation. The decision rule is: parsing and validation must finish before the first irreversible adapter call.
One-process composition and lifecycle
When startup constructs dependencies in route handlers, every request can get fresh state and shutdown cannot release resources predictably.
The general rule is to create long-lived adapters once, compose the application once, and make the server lifecycle explicit. For ParcelFlow, repositories and the fake payment adapter are created before Bun.serve; the handler closes over the composed app. A realistic service also installs readiness, graceful shutdown, structured logging, and connection cleanup. The failure mode is module-level hidden state that survives tests or differs between workers. The decision rule is: startup owns construction, request code owns use, and shutdown owns release.
const deps = {
orders: memoryOrders(),
inventory: memoryInventory({ "tenant-a:parcel-small": 3 }),
payment: fakePayment(),
};
const app = createApp(deps);
const server = Bun.serve({
port: 3000,
fetch: app.fetch,
});
process.on("SIGTERM", () => server.stop());
The Bun test runner documentation covers its Jest-compatible API and TypeScript support. These snippets are designed to be runnable in a small project, but this lesson does not claim they were executed here.
bun test
bun run src/server.ts
curl -i -X POST http://localhost:3000/orders \
-H 'content-type: application/json' \
-H 'x-tenant-id: tenant-a' \
--data '{"sku":"parcel-small","quantity":1}'
Key takeaways
Without one end-to-end slice, architecture discussions optimize imaginary boundaries instead of customer evidence.
- Start ParcelFlow as a modular monolith: one deployment, explicit modules, replaceable adapters.
- Validate tenant and request shape before payment, inventory, or persistence side effects.
- Use a fake payment adapter only; a real processor is deliberately outside this course.
- Let the request-level TDD tracer bullet prove response and owned state together.
- Treat Bun compatibility and type checking as evidence obligations, not assumptions.
Checklist
If any proof remains implicit, the next refactor can break the customer path without a useful signal.
- [ ] Write the failing request test before the handler implementation.
- [ ] Confirm the valid path returns 201 and stores exactly one tenant order.
- [ ] Confirm malformed input and fake decline create no order or reservation.
- [ ] Keep HTTP, application, payment, inventory, and repository responsibilities named.
- [ ] Record that restart durability, concurrency, and real payments are still deferred.
Sources
When runtime facts lack primary citations, rapidly changing compatibility and API details can turn a useful lesson into stale guidance.