How to Build Product-wise Coupon Allocation in PostgreSQL
Cart-level coupons are easy to abuse and hard to analyze. A three-table schema — coupons, product rules, and a usage log — makes discounts product-aware and auditable.

Cart-level coupons are easy to build: knock a flat amount or percentage off the whole order total. They're also easy to abuse, hard to analyze, and impossible to control with precision. Once a coupon needs to apply to specific products — not the whole cart — the schema has to change shape.
Core Schema
A product-wise coupon system needs three tables instead of one:
- Coupons — code, discount type, discount value, and expiry.
- Coupon product rules — links a coupon to specific product IDs or category IDs, with optional quantity minimums and per-product discount caps.
- Usage log — records which coupon fired, on which order, against which product, and how much discount was applied.
Validation at Checkout
Instead of validating the coupon code once against the whole cart, the checkout flow checks whether the code is valid for each specific product in the cart. Products covered by a matching rule get the discount; everything else in the cart is skipped.
Analytics Benefits
Because every application of a coupon is logged at the product level, you can query total discount per product, average discount per order, and how often a coupon actually triggers. That's what tells you whether a coupon is moving the inventory it was meant to move, or quietly eroding margin somewhere else.
Three Design Considerations
- Coupon conflicts — when more than one rule could apply to the same product, you need a priority system: highest discount wins, first rule applied wins, or one coupon per product per order.
- Category reassignment — if a product moves categories after a rule was written against that category, the rule can silently stop matching. An audit trail on rule changes keeps that failure from going unnoticed.
- Margin checks — discount logic can (and should) check margin thresholds before applying, so a coupon can't fire below a price floor you didn't intend to allow.
Conclusion
The question changes from "is this code valid?" to "is this code valid for this specific product, given current inventory, margin, and business intent?" The schema changes required are minor, but they unlock a lot more control than a single cart-level discount field ever could.
Found this useful?
Share it with the dev community or cross-post with a canonical link back here.
Cross-posting to dev.to or Hashnode? Use this as your canonical URL: https://www.codenovix.com/blog/product-wise-coupon-allocation-postgresql
Related articles

Using Exclude and Expose in NestJS Entities to Control API Responses
A hashed password still shouldn't leave your API. class-transformer's @Exclude and @Expose decorators make an entity define what's safe to serialize, instead of trusting every controller to filter it manually.

I Added Redis Caching to My E-Commerce Backend, and the Performance Difference Surprised Me
High-read endpoints were re-running the same database joins on every request, pushing response times to 150-250ms. Here's how a cache-aside Redis layer cut that to single-digit milliseconds.

How Designing a Clean Backend Folder Structure Improved Performance and Scalability
A messy backend structure doesn't just look bad — it slows down debugging, onboarding, and scaling. Here's how reorganizing folders into Core, Config, Module, and Shared layers changed both.