"Applied Example: Task Management Platform"
Applied Example: Task Management Platform
This section applies FCA to a task management platform (think Linear, Jira, or Asana) — a system every developer has used. It demonstrates how the fractal pattern manifests at every level, from pure functions through the running platform.
The fractal at every level
L0 — Function: A pure state transition at the heart of the task domain.
/** Transition a task's state in response to a workflow event. Pure function. */function transitionTaskState(task: Task, event: TaskEvent): Task { // Interface: (Task, TaskEvent) => Task // Domain: task state machine (ADTs: TaskState, TaskEvent) // Boundary: pure — no I/O, no database, no side effects // Port: none needed — all inputs are values // Verification: call with test inputs, assert output // Observability: return value carries the transition metadata // Documentation: JSDoc + type signature}L1 — Module: Groups related transition functions.
// Interface: exported functions (transitionTaskState, validateTransition, canTransition)// Domain: all functions operate on Task + TaskEvent types// Boundary: module scope — internal helpers not exported// Port: none — pure module// Verification: import and call with test data// Observability: (inherited from L0 function traces)// Documentation: module-level JSDoc
export function transitionTaskState(task: Task, event: TaskEvent): Task { ... }export function validateTransition(from: TaskState, to: TaskState): boolean { ... }export function canTransition(task: Task, event: TaskEvent): boolean { ... }L2 — Domain: The tasks/ directory groups all task-related modules.
source/tasks/ README.md # Domain overview: task lifecycle, state machine, validation index.ts # Domain interface: re-exports public surface task-transitions.ts # State machine transitions (pure) task-transitions.test.ts # Verification — co-located task-transitions.metrics.ts # Observability — transition counts, duration histograms task-validation.ts # Input validation rules (pure) task-validation.test.ts task-queries.ts # Query builders (pure — returns query objects, not results) task-queries.test.tsL3 — Package: @taskflow/core — the domain logic library.
packages/core/ source/ README.md tasks/ # Task domain (L2) workflows/ # Workflow domain (L2) projects/ # Project domain (L2) permissions/ # Authorization domain (L2) ports/ README.md database-port.ts # interface DatabasePort { query, execute, transaction } search-port.ts # interface SearchPort { index, search, delete } notification-port.ts # interface NotificationPort { send, sendBatch } index.ts # Package interface: re-exports from all domains + ports documentation/ README.md guides/ README.md task-lifecycle.md workflow-engine.md implementing-ports.md package.jsonL4 — Service: @taskflow/api — the HTTP server that composes packages.
packages/api/ source/ README.md services/ # Composes core domains with port implementations task-service.ts # core.tasks + database + search + notifications workflow-service.ts # core.workflows + database + queue infrastructure/ # Port implementations postgres-database.ts # implements DatabasePort elasticsearch-search.ts # implements SearchPort email-notification.ts # implements NotificationPort routes/ # Thin HTTP handlers — parse, delegate, format task-routes.ts # POST /tasks, PATCH /tasks/:id → task-service workflow-routes.ts # POST /workflows → workflow-service index.ts # Composition: wire services, register routes, start server documentation/ README.mdL5 — System: The TaskFlow platform — API + worker + frontend + infrastructure.
TaskFlow Platform @taskflow/api # HTTP API service @taskflow/worker # Background job processor (workflow execution, notifications) @taskflow/frontend # React SPA (HTTP consumer, defines own types) infrastructure/ postgres # Database elasticsearch # Search engine redis # Queue + cacheL3 Component Graph
@taskflow/types (zero deps — pure type definitions) ^ Task, TaskState, TaskEvent, Workflow, Project, Permission |@taskflow/core (depends on: types) ^ Pure domain logic: tasks, workflows, projects, permissions. | No database, no network, no I/O. Accepts ports. |@taskflow/testkit (depends on: types, core) ^ Builders, assertions, harnesses for domain verification. | taskBuilder(), workflowBuilder(), assertTransitionsTo(), etc. |@taskflow/api (depends on: types, core) | HTTP server. L4 composition layer. | Wires port implementations, registers routes. |@taskflow/worker (depends on: types, core) | Background processor. L4 composition layer. | Wires queue port, executes workflows. |@taskflow/frontend (zero @taskflow deps — HTTP consumer only) React SPA. Defines own DTO types from the API interface.Boundary constraints:
@taskflow/corenever importspg,elasticsearch,nodemailer, or any I/O library. All external access goes through ports.@taskflow/apiand@taskflow/workernever import each other. They communicate through the database and queue — shared infrastructure, not shared code.@taskflow/frontendnever imports from any@taskflow/*server package. It defines its own types from the HTTP contract.
Ports in practice
The core package defines the port interfaces. Each L4 service chooses its implementations:
// In @taskflow/core — port definitioninterface DatabasePort { query<T>(sql: string, params: unknown[]): Promise<T[]>; execute(sql: string, params: unknown[]): Promise<{ rowCount: number }>; transaction<T>(fn: (tx: DatabasePort) => Promise<T>): Promise<T>;}
// In @taskflow/api — port implementation (production)class PostgresDatabase implements DatabasePort { ... }
// In @taskflow/testkit — port implementation (verification)class InMemoryDatabase implements DatabasePort { ... }The TaskService in the API accepts the port, never the implementation:
// In @taskflow/api — service layer (composition)class TaskService { constructor( private db: DatabasePort, // injected private search: SearchPort, // injected private notifications: NotificationPort, // injected ) {}
async createTask(input: CreateTaskInput): Promise<Task> { const task = core.tasks.createTask(input); // Pure domain logic await this.db.execute(/* insert */); // Through port await this.search.index('tasks', task); // Through port await this.notifications.send(/* new task */); // Through port return task; }}Co-located artifacts in practice
A single domain module with all seven artifacts:
source/tasks/ task-transitions.ts # 1. Implementation — pure state machine task-transitions.test.ts # 2. Verification — unit tests co-located task-transitions.metrics.ts # 3. Observability — transition counters, state histograms task-transitions.config.ts # 4. Configuration — max retries, timeout defaults task-transitions.example.ts # 5. Examples — usage demos, integration tests README.md # 6. Documentation — domain overview index.ts # 7. Types/Interface — re-exports public surfaceBuild tools separate:
build:runtime→ shipstask-transitions.ts,task-transitions.config.ts,index.tsbuild:test→ includes everythingbuild:observe→ extractstask-transitions.metrics.ts→ generates Grafana dashboard JSON
Testkit in practice
The testkit ships builders that mirror the domain:
import { taskBuilder, workflowBuilder, assertTransitionsTo } from '@taskflow/testkit';import { InMemoryDatabase } from '@taskflow/testkit/ports';
// Build a task in a specific state with sensible defaultsconst task = taskBuilder() .withState('in_progress') .withAssignee('user-123') .build();
// Assert a pure domain transitionassertTransitionsTo(task, { type: 'complete' }, 'done');
// Test a service with mock portsconst db = new InMemoryDatabase();const service = new TaskService(db, mockSearch, mockNotifications);const created = await service.createTask({ title: 'Test task' });assert.equal(db.queries.length, 1); // Verify port interactionSelf-Containment Pattern
Every L3 component follows:
packages/{component-name}/ source/ README.md # Architecture: modules, ports, entry point index.ts # Interface: public exports {domain}/ README.md # Domain overview {module}.ts # Implementation {module}.test.ts # Verification (co-located) {module}.metrics.ts # Observability (co-located) {module}.config.ts # Configuration (co-located) ports/ README.md # Port definitions {port-name}.ts examples/ README.md # Example index {use-case}.ts documentation/ README.md # Component front door guides/ README.md # Guide index {use-case-name}.md decisions/ README.md # Decision log NNN-{decision-name}.md package.json tsconfig.jsonThe README rule: every directory with more than one file has a README.md with frontmatter (title, scope, contents). Every README indexes its children.