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
- Spreadsheet
- Rows
- Columns
- Cells
- Values, formulas, and relationships
REBUILT MODEL
Spreadsheet as a document
- Spreadsheet document
- JSON state
- Cells keyed by coordinates
- 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.
Large edits meant touching many related records.
The client and server had to agree on partial state changes.
Reversing a multi-record change meant reconstructing its history.
One visible sheet required several queries and joins.
Partial writes could leave related values out of step.
Conflicting edits became difficult to detect clearly.
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.
{
"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
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.
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
Relational business dataUsers, permissions, ownership, and business records.
JSON spreadsheet stateOne document for ordered layout, cell values, and formulas.
Formula calculation layerExplicit evaluation and validation at defined boundaries.
Versioning and retention rulesSnapshots, undo/redo, and a practical policy for history.
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.