Skip to content

ADR-0013: Adapter-Store Pattern over Pinia

Accepted Issue Tracker

Date: 2026-03-08

Context

The frontend needs reactive state management for domain resources (projects, issues, sprints, etc.) that are fetched from a REST API, displayed in components, edited locally, and synced back.

Every domain in this application follows an identical pattern:

  1. Fetch a collection from a REST endpoint
  2. Display items in components (read-only by default)
  3. Allow local edits via a mutable copy
  4. Sync changes back via REST (create, update, delete)
  5. Handle optimistic updates and error rollback

Pinia is the standard Vue 3 state management library. Using Pinia would mean writing a full store definition for each domain, despite every store having the same shape.

Decision

We use a custom adapter-store pattern (services/adapter-store.ts + services/adapter.ts) instead of Pinia.

A factory function adapterStoreModuleFactory() generates a complete reactive store from a domain name and adapter configuration. Each domain's store.ts is ~20 lines of factory invocation, not a full store definition.

How It Works

Adapter (services/adapter.ts): Handles REST communication for a domain. Knows the API endpoint, handles camelCase ↔ snake_case transformation, constructs request payloads.

Adapter-Store (services/adapter-store.ts): Wraps an adapter with reactive state. Provides:

  • getAll — reactive list of all items
  • getById(id) — reactive single item lookup
  • getOrFailById(id) — throws if not found
  • generateNew() — create a blank item with defaults
  • retrieveAll() — fetch from API and populate store

Adapted Items: Each item in the store exposes:

  • mutable — returns a writable copy for editing (original stays frozen)
  • reset() — discard local edits, revert to server state
  • update() — sync mutable copy back to server
  • delete() — remove from server and store
  • create() — persist a new item to server

Why Not Pinia

  • Repetition: 15+ domains with identical store shapes would mean 15+ nearly-identical Pinia store files
  • Enforced immutability: The adapter-store freezes originals and forces edits through .mutable. Pinia has no built-in concept of frozen-original + mutable-copy
  • Automatic REST integration: The adapter handles API calls, transformation, and store updates in one flow. With Pinia, each store would manually wire up axios calls
  • Convention enforcement: The factory ensures every domain follows the same API. No store has a custom fetchItems() vs loadAll() vs getItems() naming divergence

Options Considered

OptionVerdictReason
Pinia stores per domainRejected15+ domains with identical shapes = 15+ nearly-identical files. No built-in immutability enforcement.
Custom adapter-store factoryAccepted~20 lines per domain. Enforced immutability. Automatic REST integration. Convention enforcement by construction.

Consequences

Positive

  • Adding a new domain requires ~20 lines of store.ts configuration, not a full store definition
  • All domains share the same API surface — consistent patterns across the entire frontend
  • Immutability is structural: you cannot accidentally mutate a store item without calling .mutable
  • camelCase ↔ snake_case conversion is automatic and invisible to components
  • Tests follow a single pattern: mock the store from __mocks__/store.ts, assert on component behavior

Negative

  • External documentation and tutorials don't apply — the pattern is fully custom
  • AI agents must be told about this pattern explicitly or they will suggest Pinia
  • Debugging state issues requires understanding the adapter-store internals
  • No Vue DevTools integration for state inspection — debugging requires breakpoints
  • The factory abstracts away store creation, which can make it harder to understand the reactive flow for newcomers

References

Architecture documentation for contributors and collaborators.