ARCHITECTURE CASE STUDY

How I Rebuilt an Excel-Like Web App After the First Architecture Failed

The first version modelled a spreadsheet as relational data. It looked sensible on paper, then version history, formulas, and undo made every change harder to reason about. The rebuild started by treating a sheet as the document it actually was.

The first architecture: correct tables, wrong shape

The original design followed a familiar database instinct. A workbook had sheets. Sheets had rows and columns. Cells carried values and formulas. That is a reasonable way to describe the pieces, but it is not how people experience a spreadsheet.

ORIGINAL MODEL

Spreadsheet as linked records

  1. Spreadsheet
  2. Rows
  3. Columns
  4. Cells
  5. Values, formulas, and relationships

REBUILT MODEL

Spreadsheet as a document

  1. Spreadsheet document
  2. JSON state
  3. Cells keyed by coordinates
  4. Values, formulas, and metadata

Users do not edit one database entity at a time. They paste a range, reorder columns, change a formula that affects a whole section, then expect the workbook to look exactly as it did a moment ago if they press undo. The state is interconnected and ordered. It behaves more like a document than a set of independent rows.

What started going wrong

The relational model was not inherently bad. It was a poor fit for the operations this product needed to perform often and safely. Each workaround solved a local issue while making the next one more expensive.

Slow operations

Large edits meant touching many related records.

Synchronization work

The client and server had to agree on partial state changes.

Undo was fragile

Reversing a multi-record change meant reconstructing its history.

State was hard to rebuild

One visible sheet required several queries and joins.

Inconsistencies appeared

Partial writes could leave related values out of step.

Changes were lost

Conflicting edits became difficult to detect clearly.

Code grew around the model

Business rules became recovery logic and glue code.

The warning sign was version history. If restoring one familiar screen means replaying a chain of low-level record changes, the persistence model is probably working against the product.

The rebuild around JSON

The replacement did not try to make every cell a first-class database record. It represented the spreadsheet as a document and persisted the document as JSON. The application could load one coherent snapshot, change it in memory, validate it, and save a new version.

JSONSpreadsheet document snapshot
{
  "sheetName": "Forecast",
  "columns": ["Month", "Revenue", "Margin"],
  "rows": [
    { "month": "Jan", "revenue": 42000, "margin": "=B2*0.31" },
    { "month": "Feb", "revenue": 45500, "margin": "=B3*0.31" }
  ],
  "settings": { "currency": "USD", "formulaMode": "standard" }
}

This was not an argument against relational databases. The database still handled users, access, workspaces, and searchable business entities well. JSON handled the object whose value came from being preserved, ordered, and changed together.

BEFORE

Relational records across related entities

  • Useful for users, ownership, and business records
  • Awkward when one spreadsheet edit touches many parts
  • State reconstruction needs several joins and rules

AFTER

Spreadsheet state as one document

  • One snapshot carries layout, values, and formulas
  • Version history records the state users recognise
  • Relational data still supports the wider product

WHERE RELATIONAL DATA STILL BELONGS

UsersPermissionsDocument ownershipMetadataBusiness records

Undo and redo became a versioning problem

That change made undo and redo much less mysterious. Instead of inventing a reverse operation for every possible edit, the application could move through known document versions. The hard work moved to defining a clean commit boundary, which is a better problem to have.

Version 41Before paste
Version 42Edit: range added
Version 43Snapshot: formula updated
Version 44Current state

Undo moves back to a known snapshot. Redo moves forward until a new edit branches history.

Undo points backward through committed states. Redo points forward until a new edit creates a new branch. It is still important to think about storage cost, concurrent editing, and how often a snapshot is taken. But the system now explains itself in the language of the feature: this is what the sheet looked like before and after a change.

When rebuilding costs less than patching

A rebuild is not automatically the brave or correct move. It becomes reasonable when each small feature needs a migration, a synchronization rule, and a special case for history. At that point the cost is no longer the next feature. It is the tax every future feature will pay.

The practical test: can the application load the complete state a user sees, apply one meaningful change, and save that state without a chain of hidden repair steps? If not, examine whether the storage model matches the behaviour you are trying to support.

What I would do differently now

A CLEARER STACK

01

Relational business dataUsers, permissions, ownership, and business records.

02

JSON spreadsheet stateOne document for ordered layout, cell values, and formulas.

03

Formula calculation layerExplicit evaluation and validation at defined boundaries.

04

Versioning and retention rulesSnapshots, undo/redo, and a practical policy for history.

05

Concurrency handlingClear conflict rules when more than one person changes a sheet.

✓

Model the behaviour first. Start with the operations users need, not just the nouns in the domain.

✓

Design history early. Versioning exposes assumptions that simple create-and-update flows hide.

✓

Keep boundaries explicit. A document can be the source of truth while relational data supports the rest of the product.

✓

Prefer explainable state. The best architecture is one a developer can reconstruct without detective work.

This article is adapted from my original Medium post, with the implementation lessons reframed as a portfolio case study.