A restaurant and order management demo built with
Deno Fresh v2, showcasing the Dynamic Consistency
Boundary (DCB) pattern from
fmodel-decider.
Deno KV serves as the event store,
with its native secondary-index and versionstamp support making it a natural fit
for DCB's sliced, tuple-based event queries.
The domain is designed using Event Modeling — a blueprint that maps out commands, events, read models, and UI interactions in a single visual artifact.
| Layer | Technology |
|---|---|
| Runtime | Deno with --unstable-kv |
| Framework | Fresh v2 (Preact, islands architecture) |
| Styling | Tailwind CSS v4 |
| Database | Deno KV (built-in, zero config) |
| Domain | @fraktalio/fmodel-decider (DCB pattern) |
| Auth | GitHub OAuth via @deno/kv-oauth |
| Validation | Zod |
| Testing | Deno.test, fast-check (property-based) |
Unlike the traditional aggregate pattern, DCB defines consistency boundaries per use case rather than per entity. Each decider focuses on a single command and declares exactly which events it needs to make its decision.
DcbDecider<Command, State, InputEvent, OutputEvent> distinguishes between
input events (what the decider reads to build state) and output events (what it
produces) at the type level. This means the compiler enforces that decide only
returns output events and evolve handles all input events — making pattern
matching exhaustive and the entire pipeline type-safe.
| Decider | Command | Reads | Produces |
|---|---|---|---|
createRestaurantDecider |
CreateRestaurantCommand |
RestaurantCreatedEvent |
RestaurantCreatedEvent |
changeRestaurantMenuDecider |
ChangeRestaurantMenuCommand |
RestaurantCreatedEvent, RestaurantMenuChangedEvent |
RestaurantMenuChangedEvent |
placeOrderDecider |
PlaceOrderCommand |
RestaurantCreatedEvent, RestaurantMenuChangedEvent, RestaurantOrderPlacedEvent |
RestaurantOrderPlacedEvent |
markOrderAsPreparedDecider |
MarkOrderAsPreparedCommand |
RestaurantOrderPlacedEvent, OrderPreparedEvent |
OrderPreparedEvent |
Notice how placeOrderDecider spans both Restaurant and Order concepts —
something that's natural in DCB but would require a saga or process manager in
the aggregate pattern.
A production-ready event-sourced repository using Deno KV with optimistic locking, flexible querying, and type-safe tag-based indexing.
The storage layout uses three key patterns:
| Index | Key Pattern | Value |
|---|---|---|
| Primary storage | ["events", eventId] |
Full event data |
| Tag index | ["events_by_type", eventType, ...tags, eventId] |
eventId (pointer) |
| Last event pointer | ["last_event", eventType, ...tags] |
eventId (mutable pointer) |
Event data is stored once; secondary indexes store only ULID pointers. The repository automatically generates all tag subset combinations (2^n - 1 indexes per event), enabling flexible querying by any combination of tag fields. Last event pointers enable optimistic locking via Deno KV versionstamp checks.
Each decider has its own repository that declares exactly which event types it needs, queried by the relevant entity IDs. This is the sliced (or vertical) approach — instead of loading all events for an aggregate, each use case loads only the minimal slice required for its decision:
createRestaurant → [(restaurantId, "RestaurantCreatedEvent")]
changeRestaurantMenu → [(restaurantId, "RestaurantCreatedEvent")]
placeOrder → [(restaurantId, "RestaurantCreatedEvent"),
(restaurantId, "RestaurantMenuChangedEvent"),
(orderId, "RestaurantOrderPlacedEvent")]
markOrderAsPrepared → [(orderId, "RestaurantOrderPlacedEvent"),
(orderId, "OrderPreparedEvent")]
Notice how placeOrder spans two entity IDs (restaurantId and orderId) to
validate menu items against the restaurant while checking order uniqueness — a
cross-entity consistency boundary that would require coordination in the
aggregate pattern but is just a wider tuple query here.
Each tuple (entityId, eventType) maps to a Deno KV secondary index, so the
repository fetches only the matching events with no full-stream scanning. The
result: every use case pays only for the events it actually reads, and adding a
new use case never widens the query of an existing one.
// Wire a decider to its sliced repository and handle a command
const repository = createRestaurantRepository(kv);
const events = await repository.handle(
createRestaurantCommand,
createRestaurantDecider,
);Deciders are tested using a Given/When/Then format powered by
DeciderEventSourcedSpec. This makes tests read like executable specifications:
Deno.test("Place Order - Success", () => {
DeciderEventSourcedSpec.for(placeOrderDecider)
.given([
{
kind: "RestaurantCreatedEvent",
restaurantId: restaurantId("restaurant-1"),
name: "Italian Bistro",
menu: testMenu,
final: false,
tagFields: ["restaurantId"],
},
])
.when({
kind: "PlaceOrderCommand",
restaurantId: restaurantId("restaurant-1"),
orderId: orderId("order-1"),
menuItems: testMenuItems,
})
.then([
{
kind: "RestaurantOrderPlacedEvent",
restaurantId: restaurantId("restaurant-1"),
orderId: orderId("order-1"),
menuItems: testMenuItems,
final: false,
tagFields: ["restaurantId", "orderId"],
},
]);
});Error scenarios use .thenThrows():
DeciderEventSourcedSpec.for(placeOrderDecider)
.given([])
.when(placeOrderCommand)
.thenThrows((error) => error instanceof RestaurantNotFoundError);Views are pure Projection functions that fold events into denormalized
read-model state. Two views exist — orderView and restaurantView — each
handling only the events it cares about, with exhaustive pattern matching.
At runtime, an EventSourcedQueryHandler wires a view to Deno KV via
DenoKvEventLoader, building the projection on demand from stored events (no
separate read database needed).
Views are tested with a Given/Then format using ViewSpecification:
Deno.test("Order View - Order Prepared Event", () => {
ViewSpecification.for(orderView)
.given([
{
kind: "RestaurantOrderPlacedEvent",
orderId: orderId("order-1"),
restaurantId: restaurantId("restaurant-1"),
menuItems: testMenuItems,
final: false,
tagFields: ["restaurantId", "orderId"],
},
{
kind: "OrderPreparedEvent",
orderId: orderId("order-1"),
final: false,
tagFields: ["orderId"],
},
])
.then({
orderId: orderId("order-1"),
restaurantId: restaurantId("restaurant-1"),
menuItems: testMenuItems,
status: "PREPARED",
});
});├── lib/ # Domain logic (event-sourced, pure functions)
│ ├── api.ts # Shared types: branded IDs, commands, events, errors
│ ├── *Decider.ts # Use-case deciders (pure decide + evolve)
│ ├── *Repository.ts # Deno KV-backed repositories (one per decider)
│ ├── *View.ts # Read-model projections
│ ├── *ViewEventLoader.ts # Wire views to KV event storage
│ └── *_test.ts # Co-located tests
├── routes/ # Fresh file-system routing
│ ├── api/ # JSON API endpoints
│ │ ├── restaurant/ # Restaurant CRUD
│ │ ├── order/ # Order management
│ │ ├── kitchen/ # Kitchen operations
│ │ └── me/ # Current user info
│ ├── dashboard/ # Protected dashboard page
│ ├── restaurant/ # Restaurant management page
│ ├── order/ # Order management page
│ └── kitchen/ # Kitchen dashboard page
├── islands/ # Interactive Preact components (client-hydrated)
├── components/ # Static Preact components (server-rendered)
├── middleware/ # Session and auth middleware
├── utils/ # Auth, DB, error helpers
├── assets/styles.css # Tailwind CSS entry point
├── static/ # Static assets (fonts, logos)
├── main.ts # Server entry point
├── client.ts # Client entry point
├── deno.json # Config, deps, tasks
└── vite.config.ts # Vite + Fresh + Tailwind config
- Deno installed
- A GitHub OAuth App for authentication
-
Clone the repo and create a
.envfile with your GitHub OAuth credentials:GITHUB_CLIENT_ID=your_client_id GITHUB_CLIENT_SECRET=your_client_secret
-
Start the dev server:
deno task dev
# Development server
deno task dev
# Production build + start
deno task build
deno task start
# Lint, format, and type check
deno task check
# Run all tests
deno test --allow-all --unstable-kv
# Run a specific test file
deno test --allow-all --unstable-kv lib/placeOrderDecider_test.ts

