Normalization is a spectrum, not a rule
Database normalization is usually taught as a ladder you climb: first normal form, second, third, stop when the anomalies are gone. The framing implies a correct answer. There isn't one — there's a spectrum, and where you sit on it is a workload decision, not a correctness decision.
The actual trade
Normalization trades read cost for write integrity. Every level of normalization removes a class of update anomaly by adding a class of join. Denormalization runs the trade in reverse: you pre-pay joins at write time and accept that the same fact now lives in more than one place.
Once you see it as a trade, the design question changes from "is this schema normalized?" to "who pays, and when?"
- Write-heavy, integrity-critical (ledgers, inventory, identity): pay at read time. Normalize aggressively; joins are cheaper than reconciliation.
- Read-heavy, latency-critical (feeds, dashboards, catalogs): pay at write time. Denormalize deliberately, and treat the duplicated fact as a cache with an owner and an invalidation story.
- Analytical: the star schema is institutionalized denormalization — dimensions are wide on purpose because scan-and-aggregate is the workload.
The rule I actually use
Normalize until it hurts the workload you actually have, then denormalize exactly where it hurts — and write down who owns each duplicated fact.
The last clause is the one teams skip. Denormalization without a documented owner for each duplicate is how data quality dies: not in one catastrophic decision but in a hundred quiet inconsistencies nobody is responsible for.
Where this shows up beyond databases
The same trade appears anywhere state is shared: microservice data ownership (a service caching another's data is denormalizing), event-sourced projections (every projection is a denormalized read model), and agent memory systems, where the question of which facts to duplicate into working context is the normalization question in a new costume.