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.