Basics

Data Modeling

Learn how to manage state and design client-side data models in frontend system design interviews

Managing State & Data Flow

In this phase of your system design interview, it's time to dive into how you'll manage state and how you'll structure data on the client side. This is where your architecture gets real — no more boxes and arrows. You're now shaping the data and interactions that power your UI.

Spend about 15% of your total interview time here. It's a critical yet often overlooked section.


State Management in the Frontend

State management is one of the most debated areas in frontend engineering. There's no one-size-fits-all solution — it depends on your app's complexity, scale, and performance needs.

Here are some commonly used tools and patterns:

ToolWhen to Use
useStateLocal UI state (e.g., button toggles, input values)
useContextLight global state (e.g., logged-in user, language preference)
ReduxLarge apps with complex shared state and frequent updates
ZustandLightweight, scalable global store — great for dashboards or internal tools

Start simple. You rarely need Redux out of the gate. Most projects can get far with useState + useContext, and move to external tools only if the app grows more complex.


useContext vs Redux: How to Choose

FeatureuseContextRedux
SetupComes with ReactRequires external libraries
Learning CurveVery approachableSteep and opinionated
Debugging ToolsBasicPowerful devtools
BoilerplateMinimalVerbose
Re-rendersRe-renders all consumersCan optimize granular updates

Use useContext for small to medium apps. Reach for Redux only when dealing with multiple complex state transitions shared across many components.


Persistent Client State

Sometimes you want the app to remember user data after a refresh — like saved filters or cart contents. That's when browser storage helps:

Storage TypeWhen to UseCaveats
Local StoragePersist preferences, tokens, or cached dataSync API, plain string only
Session StorageTemporary state (e.g., onboarding progress)Clears on tab close
IndexedDBOffline-first apps (e.g., note-taking, CRMs)Complex, async, needs wrapper libs
CookiesAuth/session info sent with requestsSmall size, use only when necessary

Example: For a recipe planner app, use localStorage for favorite recipes, and IndexedDB for offline access to full ingredient lists.


Scoped vs Global State

Not everything needs to be global. Local state helps reduce coupling and unnecessary re-renders.

TypeExampleRe-render Behavior
Scoped StateForm values, tabs, modals, togglesAffects only the owning component and its children
Global StateAuth data, cart items, user preferencesRe-renders every component that subscribes to the shared state

Example: When building a collaborative whiteboard app, tool selection (pen, shape, color) should live locally, while the canvas data and connected users list go into global state.


Designing the Client Data Model

Now it's time to model your app's data. Start by identifying your core entities and build a type-safe shape for them. Let's imagine you're designing a project management dashboard like Trello or Asana.

interface AppState {
  user: User;
  boards: Board[];
  tasksById: Record<string, Task>;
  selectedBoardId: string;
}

interface User {
  id: string;
  name: string;
  email: string;
}

interface Board {
  id: string;
  name: string;
  memberIds: string[];
  taskIds: string[];
}

interface Task {
  id: string;
  title: string;
  description?: string;
  status: 'todo' | 'in-progress' | 'done';
  assigneeId?: string;
  dueDate?: string;
}

This structure lets you render dashboards, filter tasks, and support team collaboration — while keeping your data clear and easy to manipulate.


Normalize Your Data

Nesting data works for small apps but quickly becomes unmanageable. Normalize it to avoid duplication and stale state.

Nested Data (Anti-pattern)

const board = {
  id: 'b1',
  name: 'Q2 Planning',
  tasks: [
    {
      id: 't1',
      title: 'Budget Review',
      assignee: { id: 'u1', name: 'Sita' }
    }
  ]
};

Normalized Data

const users = {
  u1: { id: 'u1', name: 'Sita' },
};

const tasks = {
  t1: {
    id: 't1',
    title: 'Budget Review',
    assigneeId: 'u1',
  },
};

const boards = {
  b1: {
    id: 'b1',
    name: 'Q2 Planning',
    taskIds: ['t1']
  }
};

Normalization keeps state clean and avoids repetition. It's also easier to cache, update, and synchronize with backend APIs.


Data Flow Patterns

Unidirectional Data Flow

  • Props down, events up: Data flows from parent to child via props
  • Single source of truth: State is managed at the top level
  • Predictable updates: Changes flow through a clear path

Event-Driven Architecture

  • Custom events: Components communicate via events
  • Decoupled components: Components don't need to know about each other
  • Scalable: Easy to add new listeners without changing existing code

Observer Pattern

  • Publishers and subscribers: Components publish changes, others subscribe
  • Loose coupling: Components don't directly reference each other
  • Real-time updates: Perfect for collaborative features

State Management Best Practices

1. Keep State as Local as Possible

// Don't lift state unnecessarily
const [isModalOpen, setIsModalOpen] = useState(false); // Global

// Keep it local when possible
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);
  // ...
};

2. Use Derived State

// Don't duplicate state
const [filteredTasks, setFilteredTasks] = useState([]);

// Derive it from existing state
const filteredTasks = tasks.filter(task => task.status === 'todo');

3. Normalize Complex Data

// Nested data
const posts = [
  {
    id: 1,
    author: { id: 1, name: 'John' },
    comments: [
      { id: 1, author: { id: 2, name: 'Jane' } }
    ]
  }
];

// Normalized data
const posts = { 1: { id: 1, authorId: 1, commentIds: [1] } };
const authors = { 1: { id: 1, name: 'John' }, 2: { id: 2, name: 'Jane' } };
const comments = { 1: { id: 1, authorId: 2 } };

Summary Checklist

Before you move on to the next phase, review this:

  • You've chosen a state management strategy (with reasoning)
  • You've scoped local and global state appropriately
  • You know what client state needs to persist
  • Your entity types are clearly defined
  • Your data is normalized and maintainable

What's Next?

Up next, we'll explore Interface Design — how your frontend communicates with the backend and handles API interactions.

Next: Interface Design - Learn how to design efficient API communication patterns and handle data flow between frontend and backend.