Skip to content
Bitloops - Git captures what changed. Bitloops captures why.
HomeAbout usDocsBlog
ResourcesSystems Design & PerformanceState Management At Scale

State Management At Scale

State lives in multiple places—server, client cache, browser storage—and they don't always agree. State management at scale means defining what's authoritative and handling conflicts when replicas diverge.

9 min readUpdated March 4, 2026Systems Design & Performance

State is information that changes. Your user's shopping cart. A product's inventory. A session's authentication token. In a distributed system, state is replicated across multiple places: the server, the client cache, the client UI, even multiple clients simultaneously. Managing state at scale means keeping these replicas consistent without losing responsiveness.

Most state problems aren't about the state itself. They're about the source of truth—which replica is authoritative? What happens when replicas diverge? How do you reconcile conflicts? Understanding this matters whether you're building a React app, a backend service, or a distributed database.

State Topology

Where does state live in your system?

Server-Side State: The authoritative source. A database. A session store. The server is the single point of truth. Clients request state from the server and must trust what the server says.

Client-Side State: Local copies. The browser caches server data. The user's UI state (is a menu open?). State that only matters on the client. This state is easier to lose and harder to keep consistent.

Distributed State: Replicated across multiple places. A user's shopping cart exists on the server AND in the browser's local cache AND in the current UI. If the user adds an item, all three must update. If one falls behind, conflicts arise.

The fundamental tension: do you trust the client or the server as the source of truth?

Client-Trusting: The client is responsible for state. The server is a persistence layer. Good for offline-first apps. Bad for shared state (conflicts are rampant).

Server-Trusting: The server is authoritative. The client is a presentation layer. Good for shared state. Bad for responsiveness (every action requires a server round trip).

Most systems are hybrid. The server owns critical state (authentication, permissions). The client owns UI state (which tab is open?). Shared data is owned by the server but replicated to the client for responsiveness.

Client-Side State Management

Client-side state lives in React components, Redux stores, MobX observables, or plain JavaScript objects. The challenge is coordinating updates when multiple components depend on the same state.

Component State: State local to a component. React's useState. Easy to understand but causes "prop drilling"—passing state through many layers of components.

Centralized Stores: Redux, Zustand, MobX. All state in one place. Components subscribe to parts of the state they need. Updates are dispatched through a single mechanism.

Redux is the oldest and most established. Immutable state updates. Single source of truth. Actions and reducers. It's verbose but predictable.

// Redux
const initialState = { cart: [] };
const reducer = (state, action) => {
  if (action.type === 'ADD_ITEM') {
    return { ...state, cart: [...state.cart, action.item] };
  }
  return state;
};
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'ADD_ITEM', item: { id: 1, name: 'Widget' } });
javascript

Zustand is newer, simpler, and more flexible. Less boilerplate. Less ceremony.

// Zustand
const useStore = create((set) => ({
  cart: [],
  addItem: (item) => set((state) => ({ cart: [...state.cart, item] })),
}));
const { cart, addItem } = useStore();
addItem({ id: 1, name: 'Widget' });
javascript

MobX is reactive. You define computed values that update automatically when dependencies change. More implicit than Redux.

// MobX
class Store {
  cart = [];
  get total() { return this.cart.reduce((sum, item) => sum + item.price, 0); }
  addItem(item) { this.cart.push(item); }
}
const store = observable(new Store());
store.addItem({ id: 1, price: 10 });
console.log(store.total); // Updates automatically
javascript

None of these solve the fundamental problem: synchronizing client state with server state. They manage client-local state well. But when the server has updated data, how does it flow back to the client? When should the client trust its local copy vs. fetching fresh?

Server-Side State

The server owns state that must be shared across clients. A user's profile. Shared documents. The order of posts in a feed.

Server-side state lives in databases, caches, session stores, and message queues. The challenge is consistency: when one client updates data, other clients must see the change.

Database State: Persistent, authoritative. Slower. Queries hit the disk.

Cache State: Fast, temporary. Redis, Memcached. Requires invalidation strategy.

Session State: User-specific, temporary. Session store or distributed cache. Cleaned up on logout.

Message Queue State: Ephemeral, ordered. Event log. Processed by workers.

At scale, server-side state is rarely on a single machine. It's replicated across databases, regions, and services. This introduces replication lag: when data changes on one replica, other replicas are briefly inconsistent.

Client-Server Synchronization

The hard part: keeping client and server state synchronized when both can change independently.

Pull-Based: The client periodically asks the server for new data. Simple but inefficient. The client doesn't know when data changes, so it fetches constantly or waits too long.

setInterval(() => {
  fetch('/api/cart')
    .then(r => r.json())
    .then(data => store.setCart(data));
}, 5000); // Check every 5 seconds
javascript

Push-Based: The server notifies clients when data changes. WebSocket or Server-Sent Events. More efficient but more complex (clients must be connected).

const ws = new WebSocket('ws://server');
ws.onmessage = (event) => {
  const { type, data } = JSON.parse(event.data);
  if (type === 'CART_UPDATED') {
    store.setCart(data);
  }
};
javascript

Hybrid: Pull for most data, push for critical changes. Check for new data periodically, but also listen for important notifications.

Optimistic Updates

Responsiveness suffers if the client waits for server confirmation. Add an item to cart, wait for the API, then show it in the UI. That's slow.

Optimistic updates update the UI immediately, then sync to the server asynchronously. The user sees the change instantly. If the server rejects the update, you revert.

const addItem = async (item) => {
  // Immediate update (optimistic)
  store.cart.push(item);

  // Async sync
  try {
    const response = await fetch('/api/cart/items', {
      method: 'POST',
      body: JSON.stringify(item),
    });
    if (!response.ok) {
      // Revert on failure
      store.cart = store.cart.filter(i => i !== item);
    }
  } catch (error) {
    // Revert on error
    store.cart = store.cart.filter(i => i !== item);
  }
};
javascript

Optimistic updates require reversion logic. They work well for operations that are usually successful. They're risky for operations that might fail (invalid input, insufficient funds).

Conflict Resolution

When multiple clients change the same data simultaneously, conflicts arise. Client A edits a document while Client B edits it. Which version wins?

Last-Write-Wins (LWW): The most recent write overwrites previous writes. Simple but loses data. Client B's edit overwrites Client A's.

Operational Transformation (OT): Transform operations to account for concurrent changes. Google Docs uses this. When Client A edits character 5 and Client B edits character 7, both operations can be applied without conflict. But OT is complex to implement.

Conflict-Free Replicated Data Types (CRDTs): Data structures that can be replicated and updated independently, then merged without conflicts. A list can be replicated to each client. Each client edits its copy. When changes sync, the CRDT merges them automatically.

CRDTs don't resolve conflicts—they define merge semantics. An append-only list CRDT can't have conflicts (you just merge the lists). A counter CRDT works by tracking increments per client (no conflicts). But ordered sequences (like documents) are harder.

Explicit Merge: Ask the user to resolve conflicts. Show both versions. Let the user choose which one to keep. Common in git and collaborative tools.

The Consistency Spectrum

At one end, systems are purely client-trusting (offline-first). At the other, purely server-trusting (thin client). Most systems are somewhere in the middle.

Offline-First: The client owns data. The server is a backup. Conflicts are common. Good for apps where connectivity is intermittent. Bad for shared data.

Optimistic: The client is responsive. The server is authoritative. Optimistic updates assume success. Explicit reversion on failure. Good for most web apps.

Pessimistic: The client waits for server confirmation before showing changes. Slower but safer. Good for critical operations.

Read-Only: The client never changes data. All mutations go through the server. Simple but requires network round trips.

State Management Patterns

Redux-Like Pattern (Centralized): One store holds all state. Actions describe changes. Reducers update state. Good for large apps with complex state. Boilerplate-heavy.

Zustand-Like Pattern (Lightweight): Small stores for specific domains. Hook-based. Less boilerplate. Good for most apps.

Recoil-Like Pattern (Granular): Atoms are tiny units of state. Selectors derive computed values. Good for apps with lots of derived state.

Immer-Like Pattern (Mutable API): Use immutability under the hood but offer a mutable API for updates. Less verbose, equally safe.

// Immer with Zustand
const useStore = create(
  immer((set) => ({
    cart: [],
    addItem: (item) => set((state) => {
      state.cart.push(item); // Looks mutable, actually immutable
    }),
  }))
);
javascript

Data Fetching and Sync

TanStack Query (React Query) and SWR are libraries that solve the "fetch and sync" problem. They handle caching, revalidation, and synchronization between server and client.

const { data: user, isLoading } = useQuery('user', () => fetch('/api/user').then(r => r.json()));
javascript

Under the hood, this library manages:

  • Caching the response
  • Revalidating after a timeout
  • Revalidating on window focus
  • Preventing simultaneous requests
  • Deduplicating requests

These libraries do client-side state management implicitly, handling the messy details of synchronization.

AI-Generated Code and State Management

AI code generators tend to scatter state across components, miss opportunities for reuse, and not think about synchronization. Generated code might update the UI without updating the server, or vice versa.

Bitloops helps by enforcing state management patterns during generation. State is centralized. Updates follow consistent flows. Synchronization is explicit. The generated code manages state more carefully than ad-hoc code generation.

Frequently Asked Questions

Should I use Redux?

Redux is powerful but verbose. Use it if you have complex state with many interdependencies. Use Zustand if you want simplicity. Use local component state for UI-only state.

What's the difference between Redux and Zustand?

Redux enforces structure and immutability. It's more boilerplate but more predictable. Zustand is simpler and more flexible. Start with Zustand, move to Redux if you hit complexity issues.

How often should I synchronize client and server state?

It depends. Critical data (authentication, permissions) should be synchronized frequently. Less critical data (suggestions, recommendations) can be synced less often. Pull for polling, push for critical changes.

Can I use multiple state management solutions?

Yes. Redux for domain state. Local React state for UI state. URL state for navigation. Combine them as needed.

How do I handle stale client state?

Set a TTL on client-side cached data. Invalidate when you know it's changed. Use strategies like stale-while-revalidate (serve stale but fetch fresh). Show staleness indicators to users.

What are CRDTs and when do I need them?

CRDTs are data structures that can merge without conflicts. They're useful for offline-first apps where multiple devices edit independently. If you have constant connectivity, you probably don't need them.

Primary Sources

  • Martin Kleppmann's guide to designing state management at scale. Designing Data-Intensive Applications
  • Marc Shapiro's seminal paper on conflict-free replicated data types. CRDT Paper
  • Redux official documentation and style guide for state management. Redux Docs
  • Google's Site Reliability Engineering book on state consistency. SRE Book
  • Google SRE workbook with practical state management patterns. SRE Workbook
  • Apache Kafka documentation for distributed state and event streaming. Kafka Docs
  • Brewer's CAP theorem update addressing eventual consistency and state. CAP Twelve Years Later

Get Started with Bitloops.

Apply what you learn in these hubs to real AI-assisted delivery workflows with shared context, traceable reasoning, and architecture-aware engineering practices.

curl -sSL https://bitloops.com/install.sh | bash