Mental Model

Five concepts that explain how Symphony State works — and why.

1

Orchestration, not ownership

Symphony State does not replace your existing stores. It sits between them. Your server cache (TanStack Query, SWR) stays. Your URL params stay. Your local component state stays. Your localStorage stays. Symphony State acts as the conductor — coordinating reads, writes, and conflict resolution across all of them. Each source adapter wraps an existing store with a uniform interface: get, set, subscribe. The conductor manages the flow.
Example
// Each source remains independent
const serverSource = createAtomAdapter(initialData);
const urlSource = createUrlParamsAdapter({ parse, serialize });
const uiSource = createAtomAdapter(initialUI);

// The conductor orchestrates them
const conductor = createConductor({
  sections: [
    defineSection({ key: "products", source: serverSource }),
    defineSection({ key: "filters", source: urlSource }),
    defineSection({ key: "ui", source: uiSource }),
  ],
});
2

Staged commits

All updates within a transaction are staged, not applied immediately. When the transaction ends, changes are committed in dependency order — derived sections recompute, subscribers are notified once. This prevents cascading re-renders and stale intermediate states. A single transaction can touch multiple sections, and the UI sees a single, consistent snapshot.
Example
// All three changes commit as one wave
conductor.transaction(() => {
  conductor.getSection("filters").patch({ warehouse: "Berlin" });
  conductor.getSection("ui").patch({ selectedIds: [] });
  conductor.getSection("prefs").patch({ lastWarehouse: "Berlin" });
}, "warehouse-switch");

// Subscribers see: one notification, all values consistent
3

Dependency-driven flow

Sections can depend on other sections. Derived sections are computed from their inputs. The conductor builds a dependency graph and topologically sorts it. When a source section changes, only its dependents are recomputed — and only if their inputs actually changed. This is deterministic and efficient.
Example
defineDerivedSection({
  key: "filteredProducts",
  inputs: ["products", "filters"],
  compute: (products, filters) => {
    // Only recomputes when products or filters change
    return products.filter(p => matchesFilters(p, filters));
  },
});

defineDerivedSection({
  key: "summary",
  inputs: ["filteredProducts"],
  compute: (filtered) => ({
    total: filtered.length,
    value: filtered.reduce((s, p) => s + p.price, 0),
  }),
});
4

Reconciliation precedence

When multiple sources provide the same value — say, an optimistic UI update and a server response — who wins? The OrchestratedAdapter resolves this deterministically: 1. Filter out stale sources (configurable staleness threshold) 2. Sort by priority (higher wins) 3. Break ties by freshness (most recently updated wins) You can override the default logic with a custom reconcile function for domain-specific rules.
Example
createOrchestratedAdapter({
  instruments: [
    { id: "server", source: serverStore, priority: 10, role: "server" },
    { id: "optimistic", source: localStore, priority: 20, role: "optimistic",
      staleAfterMs: 5000 },
  ],
  writeTo: "optimistic",
});

// User edits → optimistic wins (higher priority)
// Server responds → optimistic becomes stale → server wins
// Conflict visible in Score panel
5

Observable sections

Every value in Symphony State carries its provenance. The conductor snapshot shows which sections exist, their current values, and the full transaction history. The orchestrator snapshot shows which source is currently driving the resolved value, the individual source values, their staleness, and their priority. This makes debugging trivial: you don't just see state — you see why it's that way.
Example
// Conductor snapshot
conductor.getSnapshot();
// → { sections: { products: [...], filters: {...} },
//     transactions: [{ label: "warehouse-switch",
//                      touched: ["filters", "ui", "prefs"],
//                      timestamp: 1711... }] }

// Orchestrator snapshot
orchestrator.getSnapshot();
// → { value: [...], driver: "server",
//     sources: {
//       server:     { value: [...], priority: 10, stale: false },
//       optimistic: { value: [...], priority: 20, stale: true }
//     } }

See it in action

The Inventory Demo shows all five concepts working together in a real dashboard.