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:
| Tool | When to Use |
|---|---|
useState | Local UI state (e.g., button toggles, input values) |
useContext | Light global state (e.g., logged-in user, language preference) |
| Redux | Large apps with complex shared state and frequent updates |
| Zustand | Lightweight, 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
| Feature | useContext | Redux |
|---|---|---|
| Setup | Comes with React | Requires external libraries |
| Learning Curve | Very approachable | Steep and opinionated |
| Debugging Tools | Basic | Powerful devtools |
| Boilerplate | Minimal | Verbose |
| Re-renders | Re-renders all consumers | Can 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 Type | When to Use | Caveats |
|---|---|---|
| Local Storage | Persist preferences, tokens, or cached data | Sync API, plain string only |
| Session Storage | Temporary state (e.g., onboarding progress) | Clears on tab close |
| IndexedDB | Offline-first apps (e.g., note-taking, CRMs) | Complex, async, needs wrapper libs |
| Cookies | Auth/session info sent with requests | Small 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.
| Type | Example | Re-render Behavior |
|---|---|---|
| Scoped State | Form values, tabs, modals, toggles | Affects only the owning component and its children |
| Global State | Auth data, cart items, user preferences | Re-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.