Questions

Design Google Sheets

Build a mini spreadsheet like Google Sheets with support for rich cell formatting, formulas, and live evaluation.

Design Google Sheets

Build a mini spreadsheet like Google Sheets with support for rich cell formatting, formulas, and live evaluation.


Problem Statement

Design a basic spreadsheet editor similar to Google Sheets with the following capabilities:

  • Editable grid of cells with dynamic sizing
  • Rich formatting (bold, italic, underline, font size, colors)
  • Cell formulas referencing other cells (e.g., =A1 + B2)
  • Circular dependency detection to prevent infinite loops
  • Live formula evaluation with real-time updates
  • Undo/Redo functionality for user actions
  • Data persistence and export capabilities

Clarifying Questions

Functional Requirements

  • Grid Size: What's the default number of rows/columns? Should it be expandable?
  • Formula Support: Should it support only basic math or advanced functions like SUM, AVG, COUNT, etc.?
  • UI Features: Will there be a formatting toolbar? Should it support cell merging?
  • Persistence: Is saving required? Should it support multiple file formats?
  • Reactivity: Should cell updates be instant across references?
  • Collaboration: Do multiple users need to edit simultaneously?

Technical Constraints

  • Performance: How many cells should it handle efficiently?
  • Browser Support: What browsers need to be supported?
  • Mobile Support: Should it work on touch devices?
  • Offline Support: Should it work without internet connection?

High-Level Architecture

1. Data Model

interface CellStyle {
  bold: boolean;
  italic: boolean;
  underline: boolean;
  fontSize: number;
  textColor: string;
  backgroundColor: string;
  textAlign: 'left' | 'center' | 'right';
  verticalAlign: 'top' | 'middle' | 'bottom';
}

interface Cell {
  id: string; // e.g., "A1", "B2"
  value: string; // Display value
  formula?: string; // Raw formula (e.g., "=A1+B2")
  computedValue?: string; // Calculated result
  style: CellStyle;
  isEditing: boolean;
}

interface Spreadsheet {
  id: string;
  name: string;
  grid: Cell[][];
  activeCell?: string;
  selection?: {
    start: string;
    end: string;
  };
}

2. Core Components

Grid Component

  • Renders the spreadsheet grid
  • Handles cell selection and navigation
  • Manages viewport and scrolling
  • Implements virtual scrolling for large grids

Cell Component

  • Individual cell rendering
  • Handles cell editing and formatting
  • Manages focus and keyboard events
  • Displays computed values vs raw formulas

Toolbar Component

  • Formatting controls (bold, italic, etc.)
  • Formula bar for editing
  • Undo/Redo buttons
  • Save/Export options

Formula Evaluation System

Core Logic

The formula evaluation system is the heart of the spreadsheet. Here's how it works:

interface FormulaContext {
  getCellValue: (cellId: string) => number;
  visited: Set<string>;
  maxDepth: number;
}

const evaluateFormula = (
  formula: string,
  context: FormulaContext,
  depth: number = 0
): number => {
  // Prevent infinite recursion
  if (depth > context.maxDepth) {
    throw new Error("Maximum formula depth exceeded");
  }

  try {
    // Replace cell references with their values
    const replaced = formula.replace(/[A-Z]+\d+/g, (match) => {
      // Check for circular dependencies
      if (context.visited.has(match)) {
        throw new Error(`Circular dependency detected: ${match}`);
      }

      context.visited.add(match);
      const cellValue = context.getCellValue(match);
      context.visited.delete(match);
      
      return cellValue.toString();
    });

    // Safely evaluate the formula
    return evaluateExpression(replaced);
  } catch (error) {
    console.error(`Formula evaluation error: ${error.message}`);
    return NaN;
  }
};

const evaluateExpression = (expression: string): number => {
  // Use Function constructor for safer evaluation
  // This is more secure than eval() but still be careful
  try {
    return new Function(`return ${expression}`)();
  } catch (error) {
    throw new Error(`Invalid expression: ${expression}`);
  }
};

Advanced Functions

const builtInFunctions = {
  SUM: (args: number[]) => args.reduce((sum, val) => sum + val, 0),
  AVERAGE: (args: number[]) => args.reduce((sum, val) => sum + val, 0) / args.length,
  COUNT: (args: any[]) => args.filter(arg => arg !== null && arg !== undefined).length,
  MAX: (args: number[]) => Math.max(...args),
  MIN: (args: number[]) => Math.min(...args),
  IF: (condition: boolean, trueValue: any, falseValue: any) => 
    condition ? trueValue : falseValue,
};

const evaluateFunction = (functionName: string, args: any[]): number => {
  const func = builtInFunctions[functionName.toUpperCase()];
  if (!func) {
    throw new Error(`Unknown function: ${functionName}`);
  }
  return func(args);
};

Styling and Formatting

Cell Styling System

const applyStyle = (cell: Cell, styleProperty: keyof CellStyle, value: any): Cell => {
  return {
    ...cell,
    style: {
      ...cell.style,
      [styleProperty]: value,
    },
  };
};

const getCellStyle = (cell: Cell): React.CSSProperties => {
  return {
    fontWeight: cell.style.bold ? 'bold' : 'normal',
    fontStyle: cell.style.italic ? 'italic' : 'normal',
    textDecoration: cell.style.underline ? 'underline' : 'none',
    fontSize: `${cell.style.fontSize}px`,
    color: cell.style.textColor,
    backgroundColor: cell.style.backgroundColor,
    textAlign: cell.style.textAlign,
    verticalAlign: cell.style.verticalAlign,
    border: '1px solid #ccc',
    padding: '4px 8px',
    minWidth: '80px',
    minHeight: '30px',
    outline: 'none',
  };
};

Formatting Toolbar

const FormattingToolbar: React.FC<{
  selectedCell: Cell | null;
  onStyleChange: (property: keyof CellStyle, value: any) => void;
}> = ({ selectedCell, onStyleChange }) => {
  return (
    <div className="formatting-toolbar">
      <button
        onClick={() => onStyleChange('bold', !selectedCell?.style.bold)}
        className={selectedCell?.style.bold ? 'active' : ''}
      >
        B
      </button>
      <button
        onClick={() => onStyleChange('italic', !selectedCell?.style.italic)}
        className={selectedCell?.style.italic ? 'active' : ''}
      >
        I
      </button>
      <button
        onClick={() => onStyleChange('underline', !selectedCell?.style.underline)}
        className={selectedCell?.style.underline ? 'active' : ''}
      >
        U
      </button>
      
      <select
        value={selectedCell?.style.fontSize || 14}
        onChange={(e) => onStyleChange('fontSize', parseInt(e.target.value))}
      >
        <option value={12}>12px</option>
        <option value={14}>14px</option>
        <option value={16}>16px</option>
        <option value={18}>18px</option>
        <option value={20}>20px</option>
      </select>
      
      <input
        type="color"
        value={selectedCell?.style.textColor || '#000000'}
        onChange={(e) => onStyleChange('textColor', e.target.value)}
        title="Text Color"
      />
      
      <input
        type="color"
        value={selectedCell?.style.backgroundColor || '#ffffff'}
        onChange={(e) => onStyleChange('backgroundColor', e.target.value)}
        title="Background Color"
      />
    </div>
  );
};

State Management and Reactivity

Grid State Management

const useSpreadsheet = (initialGrid: Cell[][] = []) => {
  const [grid, setGrid] = useState<Cell[][]>(initialGrid);
  const [activeCell, setActiveCell] = useState<string | null>(null);
  const [history, setHistory] = useState<SpreadsheetState[]>([]);
  const [historyIndex, setHistoryIndex] = useState(-1);

  const updateCell = useCallback((cellId: string, updates: Partial<Cell>) => {
    setGrid(prevGrid => {
      const [col, row] = parseCellId(cellId);
      const newGrid = prevGrid.map(row => [...row]);
      
      newGrid[row][col] = {
        ...newGrid[row][col],
        ...updates,
      };
      
      return newGrid;
    });
  }, []);

  const recalculateFormulas = useCallback(() => {
    setGrid(prevGrid => {
      return prevGrid.map(row =>
        row.map(cell => {
          if (cell.formula) {
            try {
              const result = evaluateFormula(
                cell.formula,
                {
                  getCellValue: (cellId) => getCellNumericValue(cellId, prevGrid),
                  visited: new Set(),
                  maxDepth: 100,
                }
              );
              return {
                ...cell,
                computedValue: isNaN(result) ? '#ERROR!' : result.toString(),
              };
            } catch (error) {
              return {
                ...cell,
                computedValue: '#ERROR!',
              };
            }
          }
          return cell;
        })
      );
    });
  }, []);

  // Recalculate formulas when grid changes
  useEffect(() => {
    recalculateFormulas();
  }, [grid, recalculateFormulas]);

  return {
    grid,
    activeCell,
    updateCell,
    setActiveCell,
    undo: () => {/* Implementation */},
    redo: () => {/* Implementation */},
  };
};

Complete Implementation

import React, { useState, useEffect, useRef, useCallback } from "react";

interface CellStyle {
  bold: boolean;
  italic: boolean;
  underline: boolean;
  fontSize: number;
  textColor: string;
  backgroundColor: string;
  textAlign: 'left' | 'center' | 'right';
  verticalAlign: 'top' | 'middle' | 'bottom';
}

interface Cell {
  id: string;
  value: string;
  formula?: string;
  computedValue?: string;
  style: CellStyle;
  isEditing: boolean;
}

const defaultStyle: CellStyle = {
  bold: false,
  italic: false,
  underline: false,
  fontSize: 14,
  textColor: "#000000",
  backgroundColor: "#ffffff",
  textAlign: 'left',
  verticalAlign: 'middle',
};

const parseCellId = (cellId: string): [number, number] => {
  const match = cellId.match(/^([A-Z]+)(\d+)$/);
  if (!match) throw new Error(`Invalid cell ID: ${cellId}`);
  
  const col = match[1].split('').reduce((acc, char) => 
    acc * 26 + char.charCodeAt(0) - 64, 0) - 1;
  const row = parseInt(match[2], 10) - 1;
  
  return [col, row];
};

const getCellId = (col: number, row: number): string => {
  const colStr = String.fromCharCode(65 + col);
  return `${colStr}${row + 1}`;
};

const getCellNumericValue = (cellId: string, grid: Cell[][]): number => {
  const [col, row] = parseCellId(cellId);
  const cell = grid[row]?.[col];
  if (!cell) return 0;
  
  const value = cell.computedValue || cell.value;
  const num = parseFloat(value);
  return isNaN(num) ? 0 : num;
};

const evaluateFormula = (
  formula: string,
  getCellValue: (cellId: string) => number
): number => {
  try {
    const sanitizedFormula = formula.replace(/[A-Z]+\d+/g, (match) => {
      return getCellValue(match).toString();
    });
    
    // Use Function constructor for safer evaluation
    return new Function(`return ${sanitizedFormula}`)();
  } catch (error) {
    console.error('Formula evaluation error:', error);
    return NaN;
  }
};

const CellComponent: React.FC<{
  cell: Cell;
  isActive: boolean;
  onUpdate: (updates: Partial<Cell>) => void;
  onActivate: () => void;
}> = ({ cell, isActive, onUpdate, onActivate }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState(cell.value);
  const inputRef = useRef<HTMLInputElement>(null);

  const handleDoubleClick = () => {
    setIsEditing(true);
    setEditValue(cell.formula || cell.value);
    setTimeout(() => inputRef.current?.focus(), 0);
  };

  const handleBlur = () => {
    setIsEditing(false);
    if (editValue !== cell.value) {
      if (editValue.startsWith('=')) {
        onUpdate({ value: editValue, formula: editValue.slice(1) });
      } else {
        onUpdate({ value: editValue, formula: undefined });
      }
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      handleBlur();
    } else if (e.key === 'Escape') {
      setIsEditing(false);
      setEditValue(cell.value);
    }
  };

  const displayValue = cell.formula ? cell.computedValue || '#ERROR!' : cell.value;

  return (
    <div
      className={`cell ${isActive ? 'active' : ''}`}
      onClick={onActivate}
      onDoubleClick={handleDoubleClick}
      style={{
        ...getCellStyle(cell),
        border: isActive ? '2px solid #007acc' : '1px solid #ccc',
      }}
    >
      {isEditing ? (
        <input
          ref={inputRef}
          value={editValue}
          onChange={(e) => setEditValue(e.target.value)}
          onBlur={handleBlur}
          onKeyDown={handleKeyDown}
          style={{
            width: '100%',
            height: '100%',
            border: 'none',
            outline: 'none',
            background: 'transparent',
            fontSize: cell.style.fontSize,
            fontWeight: cell.style.bold ? 'bold' : 'normal',
            fontStyle: cell.style.italic ? 'italic' : 'normal',
            textDecoration: cell.style.underline ? 'underline' : 'none',
            color: cell.style.textColor,
          }}
        />
      ) : (
        <span>{displayValue}</span>
      )}
    </div>
  );
};

const SpreadsheetApp: React.FC = () => {
  const [grid, setGrid] = useState<Cell[][]>(
    Array.from({ length: 10 }, (_, row) =>
      Array.from({ length: 10 }, (_, col) => ({
        id: getCellId(col, row),
        value: "",
        style: { ...defaultStyle },
        isEditing: false,
      }))
    )
  );

  const [selectedCell, setSelectedCell] = useState<string | null>(null);

  const getCellValue = useCallback((cellId: string): number => {
    const [col, row] = parseCellId(cellId);
    const cell = grid[row]?.[col];
    if (!cell) return 0;
    
    const value = cell.computedValue || cell.value;
    const num = parseFloat(value);
    return isNaN(num) ? 0 : num;
  }, [grid]);

  const updateCell = useCallback((cellId: string, updates: Partial<Cell>) => {
    setGrid(prevGrid => {
      const [col, row] = parseCellId(cellId);
      const newGrid = prevGrid.map(row => [...row]);
      
      newGrid[row][col] = {
        ...newGrid[row][col],
        ...updates,
      };
      
      return newGrid;
    });
  }, []);

  const applyStyle = useCallback((property: keyof CellStyle, value: any) => {
    if (!selectedCell) return;
    
    updateCell(selectedCell, {
      style: {
        ...grid[parseCellId(selectedCell)[1]][parseCellId(selectedCell)[0]].style,
        [property]: value,
      },
    });
  }, [selectedCell, grid, updateCell]);

  // Recalculate formulas when grid changes
  useEffect(() => {
    setGrid(prevGrid => {
      return prevGrid.map(row =>
        row.map(cell => {
          if (cell.formula) {
            try {
              const result = evaluateFormula(cell.formula, getCellValue);
              return {
                ...cell,
                computedValue: isNaN(result) ? '#ERROR!' : result.toString(),
              };
            } catch (error) {
              return {
                ...cell,
                computedValue: '#ERROR!',
              };
            }
          }
          return cell;
        })
      );
    });
  }, [grid, getCellValue]);

  return (
    <div style={{ padding: "20px" }}>
      <div style={{ marginBottom: "10px" }}>
        <button onClick={() => applyStyle("bold", !grid[0]?.[0]?.style.bold)}>B</button>
        <button onClick={() => applyStyle("italic", !grid[0]?.[0]?.style.italic)}>I</button>
        <button onClick={() => applyStyle("underline", !grid[0]?.[0]?.style.underline)}>U</button>
        <input
          type="color"
          onChange={(e) => applyStyle("textColor", e.target.value)}
          title="Text Color"
        />
        <input
          type="color"
          onChange={(e) => applyStyle("backgroundColor", e.target.value)}
          title="Background Color"
        />
        <select
          onChange={(e) => applyStyle("fontSize", parseInt(e.target.value))}
        >
          <option value={12}>12px</option>
          <option value={14}>14px</option>
          <option value={16}>16px</option>
          <option value={18}>18px</option>
          <option value={20}>20px</option>
        </select>
      </div>
      
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "repeat(10, auto)",
          gap: "1px",
          border: "1px solid #ccc",
        }}
      >
        {grid.map((row, rowIndex) =>
          row.map((cell, colIndex) => (
            <CellComponent
              key={cell.id}
              cell={cell}
              isActive={selectedCell === cell.id}
              onUpdate={(updates) => updateCell(cell.id, updates)}
              onActivate={() => setSelectedCell(cell.id)}
            />
          ))
        )}
      </div>
      
      <div style={{ marginTop: "20px", padding: "10px", backgroundColor: "#f5f5f5" }}>
        <strong>Instructions:</strong>
        <ul>
          <li>Click a cell to select it</li>
          <li>Double-click to edit</li>
          <li>Use formulas like =A1+B2</li>
          <li>Use the toolbar to format cells</li>
        </ul>
      </div>
    </div>
  );
};

export default SpreadsheetApp;

Best Practices

Performance Optimization

  • Virtual Scrolling: For large grids, implement virtual scrolling
  • Debounced Updates: Debounce formula recalculations
  • Memoization: Cache formula results to avoid redundant calculations
  • Lazy Loading: Load only visible cells

User Experience

  • Keyboard Navigation: Support arrow keys, Tab, Enter for navigation
  • Copy/Paste: Implement clipboard functionality
  • Undo/Redo: Track user actions for undo/redo
  • Auto-save: Save changes automatically

Error Handling

  • Circular Dependencies: Detect and prevent infinite loops
  • Invalid Formulas: Provide clear error messages
  • Data Validation: Validate cell inputs
  • Graceful Degradation: Handle edge cases gracefully

Interview Tips

What Interviewers Look For:

  1. Formula evaluation logic and circular dependency handling
  2. State management for complex grid data
  3. Performance considerations for large datasets
  4. User experience and interaction design

Sample Questions:

  • "How would you handle circular dependencies?"
  • "What if you have 10,000 cells with formulas?"
  • "How would you implement real-time collaboration?"
  • "How would you optimize rendering performance?"

Key Points to Mention:

  • Start with simple formulas, then add complexity
  • Consider edge cases (circular dependencies, invalid formulas)
  • Plan for scalability (virtual scrolling, memoization)
  • Think about user experience (keyboard shortcuts, undo/redo)

Next: Pinterest Design - Learn how to build a responsive image grid with infinite scrolling.