When to Break Database Rules:JSONB, Counters, Cents
When to break database rules: the three exceptions that survive scrutiny — JSONB for truly flexible data, denormalized counters, and money as integer cents.
Knowing when to break database rules means having a specific, documented reason — and in practice, only three reasons survive scrutiny. Genuinely flexible data can live in JSONB instead of rigid tables. Counters that would be too expensive to compute live can be stored. And money is always stored as whole cents. Everything else stays strictly structured.
I have spent a lot of words preaching structure: every fact in one place, everything connected by references, no shortcuts. That rule — the 90%+ discipline covered in database normalization explained — built a platform where hundreds of tables stay in sync with zero duplicate data. This post is about the other side: the 10%, where breaking the rules is not a shortcut but the correct engineering decision.
When to break database rules: the rule before the exceptions
First, the frame. In my platform, probably 95% of the data is relational — proper tables, proper references, proper structure. The exceptions are exactly that: exceptions, each with a written reason. That ordering matters, because AI will offer you rule-breaking constantly, and “it's simpler” is never the reason. The danger is not the exception; it is the undocumented exception that nobody can later distinguish from a mistake.
With that said: here are the three rule-breaks I use on purpose.
Exception 1: JSONB for truly flexible data
PostgreSQL — the database engine under most modern SaaS products — has a superpower called JSONB. It lets one column hold an entire free-form document: any fields, any shape, different for every row. It is the structured world's escape hatch.
Think of your database as a filing cabinet with labeled drawers. JSONB is the drawer labeled “miscellaneous” — and a miscellaneous drawer is genuinely useful, as long as it never becomes where you keep your passport. The legitimate contents: things whose shape you cannot know in advance. If your users build custom forms, every submission has different fields — you cannot pre-design a drawer for fields that do not exist yet. The same goes for user-defined custom fields, per-user preferences and settings, payloads arriving from other companies' systems, loose metadata, and the before/after snapshots inside audit logs.
The one-sentence rule I follow:
If you need to connect it to other data, query it frequently, or enforce relationships — make it relational. If it is flexible, user-defined, or rarely queried — JSONB is fine.
| JSONB is right for | JSONB is a mistake for |
|---|---|
| Form responses (every form is different) | Core business entities: users, organizations, projects |
| User-defined custom fields | Anything you query or filter constantly |
| Preferences and configuration | Data with relationships to other records |
| Incoming payloads from external systems | Data whose correctness the schema should enforce |
| Audit-log before/after snapshots | Anything feeding reports and analytics |
The tell that JSONB is safe: the data is owned by a single row and never queried independently of it. That is exactly the situation where the update problems that structure exists to prevent cannot occur. The tell that it is a mistake: you catch yourself wanting to ask questions across the JSONB — “all tasks tagged urgent,” “all orders for this customer.” Those are relationships, and relationships belong in tables.
12 weeksYour corporate career was never the planApply to the Solopreneur Accelerator.Apply now →Exception 2: counters you cannot afford to compute live
Strict structure says: never store what you can calculate. A channel's member count, a listing's average rating, a job post's application count — all of these can be computed on demand by counting the underlying records, and at small scale that is exactly what you should do.
Then scale arrives. A community where channels hold 100k posts cannot recount them on every page render. A marketplace with 500k listings cannot recompute average ratings inside every search result. A job board where companies receive 50k applications cannot count them live on every profile view. At that point you store the number — a pre-computed counter column — and reading it costs nothing.
Here is the discipline that keeps this from becoming rot: every stored counter needs a maintenance mechanism — a database trigger or function that updates it the instant the underlying data changes. A counter without one is not an optimization; it is a lie waiting to be believed. In my schema, every single counter column has a documented mechanism that keeps it in sync, and the design notes record which one and when it fires.
One refinement worth stealing: view counts are the exception within the exception. They are updated asynchronously — fired from the page after it loads — rather than by a trigger, so counting views never slows a page down, bots can be filtered out, and repeat views can be deduplicated. Exact counters (members, votes, applications) get triggers; vanity counters get the relaxed treatment.
Exception 3: money is always integer cents
This one is less “breaking a rule” than refusing a default that looks natural and is quietly dangerous. Storing €12.99 as the decimal number 12.99 invites the computer's floating-point arithmetic into your finances — and floating-point math produces tiny rounding errors that are merely ugly in a chart and unacceptable on an invoice.
The fix: store every monetary amount as a whole number of cents. €12.99 is stored as 1299. Whole numbers have no rounding errors, ever. Stripe's entire payment system speaks this convention — amounts in the smallest currency unit — so your database matches your payment provider natively. Division by 100 happens only at the last moment, in the display layer, where a mistake costs a cosmetic bug instead of money.
In my schema this is absolute: every price, fee, tip, and total is integer cents. No decimals for money, anywhere, ever.
The meta-rule: exceptions are written down
Notice what all three exceptions share. Each has a narrow trigger (unknowable shape; measured cost of live computation; the nature of money). Each has a compensating mechanism (the JOIN test; sync triggers; display-layer conversion). And each is documented — my platform keeps a design-decisions file listing every intentional deviation, precisely so that future audits (human or AI) can tell deliberate engineering from accumulated sloppiness.
That is the real difference between a professional rule-break and technical debt: one comes with a written reason, the other comes with a shrug.
The bookThe Solopreneur RevolutionLuis Gonçalves on building a solopreneur business with AI while employed.Get the book →Frequently asked questions
Is JSONB bad practice?
No — misused JSONB is bad practice. For genuinely flexible, row-owned data it is the correct tool and avoids pointless complexity. The failure mode is using it to dodge database design: shoving tags, assignees, or relationships into a JSON blob because designing the proper tables felt like work. Apply the JOIN test and JSONB stays a tool instead of a trap.
How do I know when a counter should be stored?
When live counting measurably hurts — a page that recounts large record sets on every render — and not before. Start by computing counts on demand; store a counter only when scale forces it, and never without the trigger or function that keeps it accurate. Premature counters are unearned complexity.
Why not just use a decimal column for money?
Because integer cents make an entire class of financial bug impossible rather than unlikely, and because your payment provider already thinks in smallest-currency-units. Two representations of money — cents at Stripe, decimals in your database — means conversions everywhere, and every conversion is a place to be wrong about someone's invoice.
What happens if I use JSONB for everything?
You get a database that cannot answer questions. No relationships, no integrity enforcement, no efficient filtering, painful reporting — you have rebuilt a pile of documents with extra steps. Flexibility you did not need, purchased with the queryability your business runs on. Keep the structure for the 95% and spend JSONB where shape is genuinely unknowable.
How trapped are you, really?
Take the Corporate Suffocation Index, the free solopreneur readiness assessment.