Design an enterprise data table
Lead a frontend system design interview for an enterprise data table. Cover requirements, state ownership, data fetching, rendering, accessibility, performance, testing, and rollout.
Answer Strategy
An enterprise data table is a state and data-contract problem. The UI is only one part: users expect URL-shareable filters, stable sorting, pagination, row selection, column visibility, loading and empty states, keyboard navigation, and performance that does not collapse on large datasets.
A senior design separates the table engine from the renderer. The engine takes rows plus table state and returns a deterministic view model. The React component can then focus on rendering, focus, selection controls, and communicating loading/error states.
The playground below implements the core engine. This is the logic you can actually run in an interview: filter first, sort second, clamp pagination third, derive selection and summaries last. From there you can discuss when to move filtering/sorting/pagination to the server.
Reference Implementation: Data Table View Engine
This pure TypeScript function is intentionally framework-free. In a React app, URL state, server data, and a table component would all call this same deterministic boundary.
type EmployeeRow = {
id: string;
name: string;
team: string;
salary: number;
updatedAt: string;
};
type SortKey = 'name' | 'team' | 'salary' | 'updatedAt';
type SortDirection = 'asc' | 'desc';
type TableState = {
query: string;
sort: { key: SortKey; direction: SortDirection };
page: number;
pageSize: number;
selectedIds: string[];
};
type TableView = {
rows: EmployeeRow[];
totalRows: number;
page: number;
pageCount: number;
selectedVisibleIds: string[];
summary: {
totalSalary: number;
};
};
function compareValue(a: string | number, b: string | number) {
if (typeof a === 'number' && typeof b === 'number') return a - b;
return String(a).localeCompare(String(b));
}
function createEnterpriseTableView(
rows: EmployeeRow[],
state: TableState
): TableView {
const query = state.query.trim().toLowerCase();
const selected = new Set(state.selectedIds);
const filtered = query
? rows.filter((row) =>
[row.id, row.name, row.team].some((value) =>
value.toLowerCase().includes(query)
)
)
: rows;
const sorted = [...filtered].sort((a, b) => {
const direction = state.sort.direction === 'asc' ? 1 : -1;
return compareValue(a[state.sort.key], b[state.sort.key]) * direction;
});
const pageSize = Math.max(1, state.pageSize);
const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
const page = Math.min(Math.max(1, state.page), pageCount);
const start = (page - 1) * pageSize;
const visibleRows = sorted.slice(start, start + pageSize);
return {
rows: visibleRows,
totalRows: sorted.length,
page,
pageCount,
selectedVisibleIds: visibleRows
.filter((row) => selected.has(row.id))
.map((row) => row.id),
summary: {
totalSalary: sorted.reduce((sum, row) => sum + row.salary, 0),
},
};
}Runnable Playground
Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.
type EmployeeRow = {
id: string;
name: string;
team: string;
salary: number;
updatedAt: string;
};
type SortKey = 'name' | 'team' | 'salary' | 'updatedAt';
type SortDirection = 'asc' | 'desc';
type TableState = {
query: string;
sort: { key: SortKey; direction: SortDirection };
page: number;
pageSize: number;
selectedIds: string[];
};
type TableView = {
rows: EmployeeRow[];
totalRows: number;
page: number;
pageCount: number;
selectedVisibleIds: string[];
summary: {
totalSalary: number;
};
};
function compareValue(a: string | number, b: string | number) {
if (typeof a === 'number' && typeof b === 'number') return a - b;
return String(a).localeCompare(String(b));
}
function createEnterpriseTableView(
rows: EmployeeRow[],
state: TableState
): TableView {
const query = state.query.trim().toLowerCase();
const selected = new Set(state.selectedIds);
const filtered = query
? rows.filter((row) =>
[row.id, row.name, row.team].some((value) =>
value.toLowerCase().includes(query)
)
)
: rows;
const sorted = [...filtered].sort((a, b) => {
const direction = state.sort.direction === 'asc' ? 1 : -1;
return compareValue(a[state.sort.key], b[state.sort.key]) * direction;
});
const pageSize = Math.max(1, state.pageSize);
const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
const page = Math.min(Math.max(1, state.page), pageCount);
const start = (page - 1) * pageSize;
const visibleRows = sorted.slice(start, start + pageSize);
return {
rows: visibleRows,
totalRows: sorted.length,
page,
pageCount,
selectedVisibleIds: visibleRows
.filter((row) => selected.has(row.id))
.map((row) => row.id),
summary: {
totalSalary: sorted.reduce((sum, row) => sum + row.salary, 0),
},
};
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
test('filters, sorts, paginates, and derives visible selection', () => {
const rows = [
{ id: 'u1', name: 'Ada', team: 'Engineering', salary: 140000, updatedAt: '2026-01-01' },
{ id: 'u2', name: 'Grace', team: 'Design', salary: 125000, updatedAt: '2026-01-03' },
{ id: 'u3', name: 'Linus', team: 'Engineering', salary: 150000, updatedAt: '2026-01-02' },
{ id: 'u4', name: 'Mina', team: 'Engineering Platform', salary: 120000, updatedAt: '2026-01-04' },
];
const view = createEnterpriseTableView(rows, {
query: 'eng',
sort: { key: 'salary', direction: 'desc' },
page: 1,
pageSize: 2,
selectedIds: ['u3', 'u4'],
});
expect(view.rows.map((row) => row.id)).toEqual(['u3', 'u1']);
expect(view.totalRows).toBe(3);
expect(view.pageCount).toBe(2);
expect(view.selectedVisibleIds).toEqual(['u3']);
expect(view.summary.totalSalary).toBe(410000);
});Interviewer Signal
Shows whether you can turn a broad product surface into a durable frontend architecture with clear contracts.
Constraints
- Spend the first five minutes on requirements and non-goals.
- Name client, server, cache, and URL state separately.
- Include accessibility, performance, and observability before the end.
Model Answer Shape
- Clarify users, scale, latency, collaboration, offline, and device constraints.
- Draw the route/component/data-flow shape before diving into component props.
- Choose explicit boundaries for API clients, cache, local state, design-system primitives, and tests.
Tradeoffs
- Generic primitives increase reuse but require stronger documentation and ownership.
- Client-side richness improves speed after load but can raise hydration and bundle costs.
- Real-time updates help freshness but complicate ordering, backpressure, and recovery.
Edge Cases
- Slow network and partial data.
- Permission changes while the user is on the page.
- Large datasets, long sessions, and stale caches.
Testing And Proof
- Contract tests for API adapters.
- Interaction tests for critical workflows.
- Performance budget and E2E scenario for the most important path.
Follow-Ups
- How would you roll this out safely to 1% of users?
- What would become a shared platform primitive after the second product adopted it?