Normalization, Denormalization, and Consistency Trade-offs

Duplication is a deliberate trade-off

Normalization reduces duplicated facts and can make updates easier to reason about. Denormalization duplicates selected values to reduce reads, simplify response assembly, or preserve a point-in-time snapshot.

Neither is always best. MongoDB supports both, and production schemas often combine them.

Example: product reference versus order snapshot

A product document represents the current catalog value:

{
  _id: ObjectId("65a000000000000000000001"),
  sku: "KB-104",
  name: "Mechanical Keyboard",
  currentPrice: 4999,
  active: true
}

An order should usually preserve the value charged at checkout:

{
  _id: ObjectId("65a000000000000000000050"),
  customerId: ObjectId("65a000000000000000000010"),
  items: [
    {
      productId: ObjectId("65a000000000000000000001"),
      sku: "KB-104",
      nameAtPurchase: "Mechanical Keyboard",
      unitPriceAtPurchase: 4599,
      quantity: 1
    }
  ],
  currency: "INR",
  total: 4599,
  status: "placed"
}

The order stores productId for navigation and also stores historical values. If the catalog price later changes to 5,499, the old order still reflects the amount agreed at checkout.

Three common duplication strategies

  1. Snapshot duplication: preserve a value as it existed at a business event, such as purchase-time price or shipping address.
  2. Read optimization: duplicate a frequently displayed field, such as an author's display name in a feed item.
  3. Computed duplication: maintain a summary such as order total or comment count to avoid repeatedly computing it.

Keeping duplicated values consistent

Choose a clear update strategy:

  • Update all copies synchronously in a transaction when the business operation requires it.
  • Update copies asynchronously with an event-driven workflow and accept eventual consistency.
  • Treat the duplicate as a historical snapshot that must not be updated.
  • Recompute derived values periodically or on demand.
  • Avoid duplication if the synchronization cost outweighs the read benefit.

Document the source of truth for every duplicated field. For example, products.currentPrice may be the current catalog price, while orders.items.unitPriceAtPurchase is the immutable purchase price.

Avoid ambiguous field names

Names such as price, name, and status may be ambiguous when duplicated. Prefer semantic names like currentPrice, unitPriceAtPurchase, nameAtPurchase, and statusAtPlacement when they communicate different lifecycle meanings.

Specifications & Requirements
  • Normalization:Reduce duplication and separate independently managed facts.
  • Denormalization:Duplicate selected facts for a deliberate read, snapshot, or computation benefit.
  • Eventual consistency:Copies may temporarily differ but converge after updates are processed.
Note

For every duplicated field, identify its source of truth and whether it is a live copy, an eventually consistent summary, or an immutable historical snapshot.

Compute a purchase total from immutable order-line snapshots
const order = {
  currency: "INR",
  items: [
    { sku: "KB-104", unitPriceAtPurchase: 4599, quantity: 1 },
    { sku: "MS-208", unitPriceAtPurchase: 799, quantity: 2 }
  ]
};

order.subtotal = order.items.reduce(
  (sum, item) => sum + item.unitPriceAtPurchase * item.quantity,
  0
);

// 4599 + (799 * 2) = 6197
Key Takeaways
  • Duplicate data only for a clear reason.
  • Keep historical snapshots distinct from current values.
  • Define consistency, update ownership, and repair behavior for denormalized fields.
Knowledge Check

Why should an order usually store the unit price at purchase time?