โ† Back to question bank
System DesignSeniorMedium#5002 ยท 50m

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),
    },
  };
}
TypeScript ยท runnable

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.

Contract
Write the smallest tests around the public API, expected return shape, and failure behavior.
Edges
Cover empty input, repeated calls, slow async work, cancellation, and malformed data.
Integration
Name the component, route, or service boundary that proves the code works in the product surface.
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?