Skip to content

Repository files navigation

Restaurant Order Management — Deno (TypeScript) Demo

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.

Event Modeling

The domain is designed using Event Modeling — a blueprint that maps out commands, events, read models, and UI interactions in a single visual artifact.

Event Modeling Blueprint

Tech Stack

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)

Dynamic Consistency Boundary (DCB)

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.

Use-Case Deciders

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.

Event Repository (Deno KV)

A production-ready event-sourced repository using Deno KV with optimistic locking, flexible querying, and type-safe tag-based indexing.

Event Repository Architecture

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.

Sliced / Vertical Repositories

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,
);

Specification by Example (Given/When/Then)

Deciders are tested using a Given/When/Then format powered by DeciderEventSourcedSpec. This makes tests read like executable specifications:

Given-When-Then Testing

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 (Ad-hoc / Live Read Models)

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",
    });
});

Project Structure

├── 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

Getting Started

Prerequisites

Setup

  1. Clone the repo and create a .env file with your GitHub OAuth credentials:

    GITHUB_CLIENT_ID=your_client_id
    GITHUB_CLIENT_SECRET=your_client_secret
  2. Start the dev server:

    deno task dev

Common Commands

# 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

Releases

Packages

Used by

Contributors

Languages