Designing Stores
The model defines the interface. This page is about the design space on the other side of it: the shapes stores take, the trade-offs they make, and the patterns that emerge when you build systems from them.
The contract
Read a path, get a value. Write a path and value, get a result path.
That's the entire surface area. But within those two operations, stores make choices that give them radically different behavior. A memory store is a tree you write values into. An HTTP store translates paths into URLs. A clock store ignores writes entirely and returns the current time on every read. All three are valid stores.
The contract says nothing about what paths mean, what values are accepted, whether reads have side effects, or whether writes are durable. Those are design choices, and they define the character of the store.
Relative addressing
A store doesn't know where it lives in the tree.
When you mount a store at /api and read /api/users, the store receives users, not /api/users. The tree strips the mount prefix before routing. The same store can be mounted at different paths without modification, and developed and tested in isolation without knowing its eventual location.
This is the foundation of testability. If a store only sees relative paths, you can test it without constructing a full tree. Mount it at the root, send it paths directly, verify the results.
Store shapes
Stores vary along a few dimensions. Understanding these dimensions helps you design stores that compose well.
Stateless
Every read is a pure function of the path. No memory between operations. A clock, a random number generator, an environment variable reader. Simple to reason about, trivially safe to share.
Stateful
Reads and writes change internal state. A memory store accumulates data. A file handle tracks a position. An HTTP broker queues requests. State means ordering matters: the same path can return different values depending on what happened before.
Proxy
Translates StructFS operations into another protocol. An HTTP store turns reads into GET requests. A database store turns paths into queries. The store is an adapter between the StructFS world and everything outside it.
Composite
Contains other stores. The tree itself is a composite: it routes paths to child stores by prefix. A cache wraps a store and intercepts reads. A logger sits in front of anything and records operations. Composites are stores made of stores.
These categories aren't exclusive. A caching proxy is both composite and proxy. An HTTP broker is stateful and proxy. The categories are a vocabulary for thinking about what a store does, not a type system to inherit from.
The broker pattern
Some operations don't fit "write a value, read it back." The broker pattern splits intent from execution.
Write expresses intent: "I want this to happen." The store returns a handle path. Read against that handle executes the intent and returns the result.
→ /http/pending/0
read /http/pending/0
→ {"status": 200, "body": {...}}
The separation gives you control over timing. Queue several writes before reading any results. Inspect the queue. Discard a pending operation by never reading its handle. The interface stays synchronous (no callbacks, no futures) but the interaction is inherently multi-step.
This pattern works for anything with deferred results: database transactions, file uploads, long-running computations. Write to start. Read to finish.
Handle-based protocols
When write returns a path, that path becomes a handle: a multi-step conversation with the store.
File I/O is the classic example. Write to an "open" path with a filename and mode. Get back a handle path. Read and write against the handle. Write to a "close" sub-path to release it.
→ /fs/handles/0
read /fs/handles/0 ← file content
read /fs/handles/0/position ← current offset
write /fs/handles/0/close null ← release
The handle is a sub-tree with its own paths. position, close, and the handle root itself are all just paths: readable, writable, composable with the same mechanisms as everything else. No special handle type, no resource lifecycle API.
This generalizes to sessions, transactions, subscriptions, iterators: any interaction spanning multiple operations. The handle path is the session. Sub-paths are the session's capabilities. In the representation hierarchy, this is where interfaces become protocols.
Self-description
A store can describe itself through the same read interface it uses for everything else.
Serve documentation at a well-known path, and any tool that reads from the tree can discover what the store does, what paths it responds to, and what values it expects. No separate introspection API, no schema registry, no documentation system outside the tree.
This is what makes the meta lens possible: reading meta/path returns information about what path does. But even without that convention, a store can put help text, schemas, or examples at any path it chooses. Self-description is a design choice, not a mandate.
Design principles
Two principles for keeping a store's interface clean.
Consistency
If reading /foo returns a map with key bar, then reading /foo/bar should return the same value. Paths should decompose predictably. Not every store can guarantee this (external systems have their own rules), but when you have a choice, prefer the more consistent option.
Concision
One way to perform a given action. If a value is readable at two paths, that's a design smell. If there are three ways to delete something, pick one. Fewer paths means fewer things to learn, test, and document.