This document explores the foundational ideas behind Reify.
Reification is the act of making something abstract into something concrete.
In philosophy, it means treating concepts as if they were real, tangible things. In programming, we extend this idea: operations themselves become tangible objects.
Consider a simple database write:
await db.user.create({ name: "Alice" });This operation executes and vanishes. It leaves behind an effect (a new row in the database), but the operation itself—the intent, the structure, the meaning—is gone forever.
Reify preserves it:
const operation = entity.create("User", { name: "Alice" });
// The operation exists as data
// It can be inspected, stored, transmitted, transformed
// And eventually, executedReify separates concerns into three distinct layers:
┌─────────────────────────────────────────────────────┐
│ BUILDER │
│ │
│ Type-safe DSL for constructing operations │
│ pipe(), entity.create(), ref(), temp()... │
│ │
└─────────────────────┬───────────────────────────────┘
│
│ produces
▼
┌─────────────────────────────────────────────────────┐
│ DATA │
│ │
│ Plain JavaScript objects (JSON-serializable) │
│ The universal representation of operations │
│ │
└─────────────────────┬───────────────────────────────┘
│
│ consumed by
▼
┌─────────────────────────────────────────────────────┐
│ EXECUTOR │
│ │
│ Plugins that interpret and execute operations │
│ execute() + plugin system │
│ │
└─────────────────────────────────────────────────────┘
The builder provides a type-safe, ergonomic interface for constructing operations. It uses TypeScript's type system to ensure correctness at compile time.
const op = entity.create("User", {
id: temp(), // TypeScript knows this is a temp reference
name: input.name, // TypeScript knows this references input
createdAt: now(), // TypeScript knows this is a timestamp
});The builder doesn't execute anything. It produces data.
The data layer is the heart of Reify. Operations exist as plain JavaScript objects that can be:
- Serialized:
JSON.stringify(operation) - Stored: Save to database, file, or any storage
- Transmitted: Send over HTTP, WebSocket, message queue
- Inspected: Log, debug, analyze
- Transformed: Modify, filter, compose
// An operation is just data
{
"$do": "entity.create",
"$with": {
"type": "User",
"name": { "$input": "name" }
},
"$as": "user"
}This data is environment-agnostic. It doesn't know or care where it will be executed.
The executor interprets operation data and produces effects. Different executors (plugins) can interpret the same operation differently:
// Same operation
const op = entity.create("User", { name: "Alice" });
// Different interpretations
cachePlugin: → Updates in-memory cache
prismaPlugin: → Writes to database via Prisma
logPlugin: → Logs the operation
mockPlugin: → Returns mock data for testingOperations often need to reference dynamic values. Reify provides several reference types:
| Reference | JSON Representation | Purpose |
|---|---|---|
input.field |
{ "$input": "field" } |
Access input data |
ref("step").field |
{ "$ref": "step.field" } |
Reference previous result |
temp() |
{ "$temp": true } |
Generate temporary ID |
now() |
{ "$now": true } |
Current timestamp |
Operators represent atomic transformations that can be applied during execution:
| Operator | JSON Representation | Semantics |
|---|---|---|
inc(1) |
{ "$inc": 1 } |
Add to current value |
dec(1) |
{ "$dec": 1 } |
Subtract from current value |
push("x") |
{ "$push": "x" } |
Append to array |
pull("x") |
{ "$pull": "x" } |
Remove from array |
addToSet("x") |
{ "$addToSet": "x" } |
Add if not present |
Conditional logic is also represented as data:
// Builder
branch(input.exists)
.then(entity.update(...))
.else(entity.create(...))
.as("result")
// Data
{
"$when": { "$input": "exists" },
"$then": { "$do": "entity.update", ... },
"$else": { "$do": "entity.create", ... },
"$as": "result"
}A pipeline is a sequence of operations that may reference each other:
const pipeline = pipe(({ input }) => [
entity.create("Order", { id: temp() }).as("order"),
entity.create("Item", { orderId: ref("order").id }).as("item"),
]);Execution proceeds sequentially, building up a context of results:
Step 1: Execute "order" operation
→ Result stored as ctx.refs["order"]
Step 2: Execute "item" operation
→ ref("order").id resolved from ctx.refs["order"]
→ Result stored as ctx.refs["item"]
Final: Return all step results
Before each step executes, all references are resolved:
{ "$input": "field" }→ Look up in input data{ "$ref": "step.field" }→ Look up in previous results{ "$temp": true }→ Generate unique ID{ "$now": true }→ Get current timestamp
Plugins register effect handlers organized by namespace:
interface Plugin {
namespace: string;
effects: {
[effectName: string]: EffectHandler;
};
}
type EffectHandler = (args: unknown, ctx: ExecutionContext) => Promise<unknown>;const entityPlugin = {
namespace: "entity",
effects: {
create: async (args, ctx) => {
// args = { type: "User", name: "Alice", ... }
// Return whatever makes sense for this environment
return { ...args, id: generateId() };
},
update: async (args, ctx) => { ... },
delete: async (args, ctx) => { ... },
},
};When an operation like entity.create executes:
- Parse effect name:
"entity.create"→ namespace"entity", effect"create" - Find plugin with matching namespace
- Call the effect handler with resolved arguments
- Store result for potential reference by later steps
Functions are opaque. You cannot inspect what a function will do without executing it. Data is transparent—you can examine, transform, and reason about it.
// Function: opaque
const fn = () => db.user.create({ name: "Alice" });
// What does this do? We must execute to find out.
// Data: transparent
const op = { type: "create", entity: "User", data: { name: "Alice" } };
// We can inspect, validate, transform before execution.Different environments have different capabilities and constraints:
- Browser: No direct database access
- Server: Full database access
- Test: No real infrastructure
- Preview: Dry-run, no side effects
Plugins allow the same operation to adapt to its environment.
Operations in a pipeline often depend on each other. Sequential execution with reference resolution provides:
- Predictability: Operations execute in defined order
- Composability: Later operations can reference earlier results
- Debuggability: Each step can be inspected independently
Redux actions are also "operations as data," but:
- Redux actions describe state transitions, Reify describes effects
- Redux requires a central store, Reify is store-agnostic
- Redux reducers are synchronous, Reify handlers are async
GraphQL mutations describe operations, but:
- GraphQL is tied to a schema and server
- Reify is runtime and environment agnostic
- Reify operations can execute anywhere, not just on a GraphQL server
Event sourcing stores events as the source of truth:
- Events describe what happened (past)
- Reify operations describe what to do (intent)
- Reify can be used to implement event sourcing
Reify is built on a simple but powerful idea: operations should be data.
This enables:
- Inspection: See what will happen before it happens
- Storage: Keep a record of all operations
- Transmission: Send operations across boundaries
- Replay: Re-execute operations from history
- Transformation: Modify operations programmatically
- Portability: Execute anywhere with appropriate plugins
The separation into Builder → Data → Executor creates a clean architecture where each layer has a single responsibility, and the data layer serves as the universal interface between intent and execution.