System Decisions
System Design
Full-Stack E-Commerce

The Cost of Abstraction in Systems

Decision summary

Implicit NoSQL schemas were abandoned in favor of rigid, compiled Data Contracts using TypeScript and Zod to prevent payment desyncs.

Invariant: Boundary-layer payloads must conform to one explicit contract.

  • Decision ID

    DEC-001

  • Status

    Active

  • Date

    Aug 2024

Where this is applied

This shows up in systems like UniConvert and TraceAI, where correctness depends on traceable outcomes.

Problem context

While building the Printish printing service platform, I needed to orchestrate file uploads, Firestore database writes, and Razorpay payment webhooks simultaneously.

Because NoSQL databases like Firestore are schemaless, any part of the codebase could theoretically write arbitrary JSON objects into the order collections.

The tempting option

The standard approach in highly agile environments is to rely on conventional typing — assuming the frontend sends the right payload and hoping the webhook receiver expects it.

This implicit abstraction allows teams to move fast, add features without migrating databases, and deploy without breaking compilation.

Why it failed

Implicit schemas fail dangerously at boundary layers, particularly financial ones.

If a developer accidentally renamed the "orderTotal" field to "amount", Firestore accepted it without complaint. However, the Razorpay webhook verification function expected "orderTotal".

When the webhook fired, it threw a silent undefined error, marking a successfully paid order as "pending" forever. The abstraction provided by NoSQL hid the fact that a critical financial contract had been broken.

The chosen approach

I implemented rigid Data Contracts using Zod and shared TypeScript interfaces across the entire monorepo.

Every write to Firestore passes through a strict Zod parsing layer. Every Razorpay webhook is validated against the exact same contract.

This explicitly trades the "schema-free" agility of NoSQL for the determinism of relational databases. If a payload shape changes, the system breaks at compile-time, not during a live payment.

Failure Modes

This approach adds considerable friction to feature development:

- Every new order property requires updating the contract, which forces recompilation across all dependent services. - The Zod parsing layer introduces minor, though measurable, compute overhead on every single read/write. - Rapid prototyping feels heavier and slower.

When to Revisit

If non-critical analytics tracking were separated from the core transactional data stream, the data contract requirement could be dropped for the analytics side.

For any systems involving real-time job queues or payments, the rigid contract pattern will not be removed.