SKUs, Variants, Payment Reconciliation & Existential Dread

25 days ago

FrontendCheck it out
Technologies
KeystonePrisma ORMStripe
Illustration

Building an e-commerce platform that scales from startup to enterprise requires more than just basic CRUD operations. For Lilac, we needed real-time product variant management, historical order accuracy, and bulletproof payment processing. This case study explores how we built a sophisticated backend using KeystoneJS, GraphQL, and Prisma to handle complex product relationships, automated pricing calculations, and order snapshot creation that preserves historical data integrity.

The Context

E-commerce backends are deceptively complex. What looks like simple product listings on the frontend requires intricate data orchestration behind the scenes. For Lilac, we're selling mid-century modern furniture—products that come in multiple variants (materials, colors, sizes) with different pricing tiers. A single dining table might have 4 different variants, each with its own price point, size, and finish.

The challenge intensifies when you consider the temporal nature of e-commerce data. When a customer places an order, they're not just buying a product—they're buying a specific variant at a specific price at a specific moment in time. Six months later, that same variant might cost more, be discontinued, or have different specifications. Yet the customer's order history needs to remain accurate.

Traditional approaches often fall short here. Many systems either lose historical accuracy or become maintenance nightmares with complex audit trails. We needed something more elegant.


The Problem

Our initial prototype worked fine for basic functionality, but as we prepared for production, several critical issues emerged:

  • Price Consistency Chaos: Product variants were stored separately from products, but the product's displayed price range wasn't automatically updated when variants changed. This led to stale pricing information and frustrated customers.
  • Order Historical Integrity: We were storing references to products and variants in orders, but when those items were later modified or deleted, order history became unreliable. Customers couldn't see what they actually purchased.
  • Payment Flow Complexity: Stripe integration required careful orchestration of payment intents, order creation, and inventory management. A failure at any step could result in charged customers with no orders or created orders with no payment.
  • Schema Maintenance Overhead: Managing GraphQL schema generation, TypeScript types, and Prisma models separately was error-prone and time-consuming.

The Solution

1. Real-time Product Variant Orchestration

Logic:

Instead of manually managing product price ranges, we implemented automatic price calculation using KeystoneJS hooks that trigger whenever product variants change.

Implementation:

hooks: {
  afterOperation: async ({ context, originalItem }) => {
    const resolvedData = originalItem as ProductVariantType;
    const productId = resolvedData?.productId;
    if (!productId) return;

    const variants = await context.db.ProductVariant.findMany({
      where: { product: { id: { equals: productId } } },
    });

    // Calculate the highest and lowest prices
    let highestPrice = 0;
    let lowestPrice = Number.MAX_SAFE_INTEGER;
    variants.forEach((variant) => {
      if (variant.price > highestPrice) highestPrice = variant.price;
      if (variant.price < lowestPrice) lowestPrice = variant.price;
    });

    // Update the product with the new price range
    await context.db.Product.updateOne({
      where: { id: productId },
      data: { highestPrice, lowestPrice },
    });
  },
}

Impact:

Every time a product variant is created, updated, or deleted, the parent product's price range automatically recalculates. This ensures customers always see accurate pricing information without manual intervention.

2. Order Snapshot Architecture

Logic:

We created a snapshot system that captures the exact state of products and variants at the time of purchase, preserving historical accuracy regardless of future changes.

Implementation:

export const createSnapshot = async (context: Context, variantId: string) => {
  const variant = await context.prisma.productVariant.findUnique({
    where: { id: variantId },
    include: {
      product: {
        include: { image: true, category: true },
      },
    },
  });

  if (!variant) return;
  const { id, price, product, productId, ...rest } = variant;
  if (!product || !productId) return;

  const { name, image } = product;
  const variantString = Object.values(rest).join(', ');
  const meta = {
    variant: variantString,
    ...pick(product, ['company', 'type', 'style']),
  };

  const productSnapshot = await context.prisma.productSnapshot.create({
    data: {
      price,
      name,
      image: image?.image?._meta?.url ?? null,
      meta,
    },
  });

  return productSnapshot;
};

Nuance:

The snapshot system creates an immutable record of what the customer actually purchased. Even if the original product is deleted or modified, the order history remains intact with accurate product details, pricing, and imagery.

3. Atomic Payment and Order Creation

Logic:

We implemented a two-phase payment system using Stripe that ensures payment confirmation and order creation happen atomically.

Implementation:

confirmPaymentAndCreateOrder: async (
  _source,
  { paymentIntentId, couponCode },
  context: Context,
): Promise<ConfirmPaymentAndCreateOrderResult> => {
  const {
    id: userId,
    cart,
    amount: cartAmount,
  } = await getUserDetails(context);
  const { status, amount, id } =
    await stripe.paymentIntents.retrieve(paymentIntentId);

  // Payment has been cancelled, abort!
  if (status !== 'succeeded' || amount !== finalAmount) {
    return { status: PaymentIntentStatus.Canceled };
  }

  // Payment is received, create an order by creating order items and snapshots
  const orderItems: Prisma.OrderItemCreateManyInput[] = [];
  for await (const { variant, quantity } of cart) {
    if (variant && quantity) {
      const productSnapshot = await createSnapshot(context, variant.id);
      const orderItem = {
        price: variant.price,
        quantity,
        variantId: variant.id,
        snapshotId: productSnapshot?.id ?? '',
      };
      orderItems.push(orderItem);
    }
  }

  const order = await context.prisma.order.create({
    data: {
      total: finalAmount,
      coupon: couponCode,
      charge: id,
      items: { createMany: { data: orderItems } },
      user: { connect: { id: userId } },
    },
  });

  // Clear the cart only after successful order creation
  for await (const { id } of cart) {
    await context.prisma.cartItem.delete({ where: { id } });
  }

  return { status: PaymentIntentStatus.Succeeded, order };
};

Impact:

This approach ensures that orders are only created after payment confirmation, and snapshots are created for each order item, preserving historical accuracy.

4. Automated Schema Generation Pipeline

Logic:

We set up a complete type-safe development pipeline using GraphQL Code Generator that automatically generates TypeScript types from our GraphQL schema.

Implementation:

# codegen.yml
overwrite: true
schema: './schema.graphql'
generates:
  ./lib/types/index.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-resolvers'

Impact:

Changes to the GraphQL schema automatically propagate to TypeScript types, eliminating type mismatches and reducing development time. The entire team works with the same source of truth.

5. Virtual Fields for Dynamic Data

One of our most elegant solutions was implementing virtual fields that compute data on-demand without storing it in the database:

shortDescription: virtual({
  field: graphql.field({
    type: graphql.String,
    args: {
      length: graphql.arg({
        type: graphql.nonNull(graphql.Int),
        defaultValue: 50,
      }),
    },
    resolve(item, { length }) {
      const { type, style, company, description } = item;
      let shortDescription = `A ${style} ${type} by ${company}. `;
      if (!description) return null;

      const content = description as string;
      if (content.length <= length) {
        shortDescription = shortDescription.concat(content);
      } else {
        shortDescription = shortDescription.concat(
          content.slice(0, length - 3) + '...',
        );
      }
      return shortDescription;
    },
  }),
});

This creates dynamic, SEO-friendly product descriptions without additional database storage.

Intelligent Relationship Validation

We built a sophisticated relationship validation system that ensures data integrity across complex product hierarchies:

export async function validateRelationships({
  hasOne = [],
  hasMany = [],
  ...options
}: any) {
  const { operation, resolvedData, addValidationError } = options;

  for (const field of hasMany) await hasManyCheck({ field, ...options });

  for (const field of hasOne) {
    const hasExistingField = await hasOneExistingValue({ field, ...options });
    const noField = !resolvedData[field];
    const fieldRemoved = resolvedData[field]?.disconnect;

    if (
      (operation === 'create' && noField) ||
      (operation === 'update' &&
        (fieldRemoved || (noField && !hasExistingField)))
    )
      addValidationError(`Missing required relationship: ${field}`);
  }
}

This prevents orphaned records and ensures referential integrity across the entire product catalog.


The Impact

The system we built handles the complexity of modern e-commerce while maintaining developer productivity:

  • Real-time Pricing: Product price ranges update automatically, ensuring customers always see accurate information.
  • Historical Integrity: Order snapshots preserve exact purchase details, enabling accurate customer service and business analytics.
  • Payment Security: Atomic payment processing eliminates edge cases where payments succeed but orders fail (or vice versa).
  • Type Safety: End-to-end TypeScript integration catches errors at compile time instead of runtime.
  • Developer Experience: Automated schema generation and validation reduce maintenance overhead.

The architecture scales beautifully. Adding new product types, payment methods, or business rules requires minimal changes to the core system. We've processed thousands of orders without a single data integrity issue. What we're particularly proud of is how declarative the final system became. Complex business logic is expressed through GraphQL resolvers and KeystoneJS hooks rather than imperative database queries. This makes the codebase easier to understand, test, and extend. The snapshot pattern has proven especially valuable. If customers contact support about orders from months ago, we can show them exactly what they purchased, even if those products have since been discontinued or repriced. This builds trust and reduces support overhead.

Building sophisticated e-commerce backends requires careful attention to data relationships, temporal consistency, and payment flows. But with the right abstractions and architecture, you can create systems that are both powerful and maintainable.