Building an Offer System That Doesn't Break When Pricing Changes

You're building a system where customers get a quoted price that's locked in for a limited time. The product catalog price might change tomorrow. The customer shouldn't be affected. This is the core problem the Offer Letter Pattern solves, and it's more annoying to implement correctly than most people expect. The Offer Letter Pattern is a design approach where you capture a snapshot of commercial terms — pricing, discounts, conditions — at a specific moment, bind them to a customer for a defined window, and allow that binding to transition from provisional to committed or expire naturally. It's not a formalized pattern with a name you'll find in GoF. You'll see it called a quote system, a negotiation envelope, or just an offer table in most codebases. The core tension is simple: prices change. Customer-facing pricing should be stable once a quote is generated. But you can't hold a price forever, or every user will sit on their quote indefinitely and never convert.

How I Built One

I spent about three weeks last year implementing this for a B2B SaaS billing platform. We sell enterprise seats with annual contracts, custom discounts per territory, and a lot of negotiation. Here's the structure that actually worked. The database model has an offers table that stores the snapshot: base_price from the catalog at the time of creation, computed_discount_percent calculated from the rep's authorization level, final_price as the immutable number the customer sees, terms_json for any conditional clauses, status tracking the lifecycle, and expires_at as the TTL. Creating an offer means reading the current catalog price, applying any applicable discount logic, writing the snapshot to the offers table, and returning it to the sales rep. The catalog price continues evolving independently. That's the whole point.

When the customer accepts, the offer transitions to committed. We then read from the offer's stored final_price, not the live catalog price, to generate the invoice. A background job handles expired offers by moving them to a completed status — we don't delete them. Auditing matters, and I've seen too many teams delete expired offers and then scramble when a customer claims they were quoted a different price last month. The acceptance flow has one tricky part: what happens if two people try to accept the same offer simultaneously? You need an optimistic lock or a unique constraint on accepting. I went with a simple SQL UPDATE where status = 'draft' AND id = $offer_id with a row count check. If zero rows were updated, someone else got there first and you return a conflict response.

Get the Full Details

Offer Letter Template and 2026 Guide [+3 Free Downloads] - AIHR
Offer Letter Template and 2026 Guide [+3 Free Downloads] - AIHR

A Real Problem I Hit

Halfway through the project, a sales rep noticed that offers with a 24-hour expiry were being accepted right before they expired, then customers were stalling on payment while the clock kept running. By the time payment completed, the offer had expired and the rep had to recreate it at a higher price because our catalog had increased monthly rates. The customer was angry, the rep lost credibility, and I had a support ticket to fix. The workaround was to separate the offer TTL from the payment window. The offer locks the price for the full expiry window, but acceptance triggers a separate payment_grace_period counter. The price stays locked for 72 hours after acceptance regardless of when the original offer was created. I added this as a configuration parameter per offer type rather than hardcoding it, because different products have different payment cycles.

Why This Is Worth the Effort

There are at least three real benefits to doing this properly. First, pricing consistency. A customer who was quoted $4800 annually sees $4800 on the invoice, even if your pricing team changed the list price the day before they signed. Second, inventory protection. When you're offering reserved capacity — cloud seats, event tickets, limited inventory — the offer acts as a soft reservation that doesn't commit until acceptance. Third, dispute resolution. When a customer says "but you quoted me X," you have a timestamped record with the exact terms that were presented. The most common mistake I see is treating the offer as a cart. A cart is a collection of items with quantities, still editable, still connected to the live catalog. An offer is a finalized quote snapshot. Mixing the two leads to situations where customers modify items in their "offer" and expect the locked price to apply to the new configuration, which defeats the entire purpose. Another mistake is making the expiry too generous. I've seen 30-day offers in B2C contexts where the product price changes weekly. The offer becomes a pricing loophole. Six hours to two days is usually the right range depending on your sales cycle. Test it against your actual conversion data rather than picking a number because it feels fair.

The third mistake is not handling partial acceptances. In B2B, a customer might accept an offer for 80 seats but push back on 20. If your system only supports accept or reject, you're forcing the sales rep to manually recreate the offer with modified quantities. A simple line-item-level acceptance flow — where each item in the offer can be accepted, rejected, or modified independently — saves a enormous amount of manual work.

Offer Letter Template and 2026 Guide [+3 Free Downloads] - AIHR
Offer Letter Template and 2026 Guide [+3 Free Downloads] - AIHR

When This Pattern Fails

The Offer Letter Pattern breaks down in a few specific scenarios, and it's worth knowing them before you invest in building one. Multi-party approval flows are the biggest one. If a quoted price needs to be approved by a manager, then legal, then finance before the customer sees the final number, the simple draft-to-committed lifecycle doesn't model that. You'll end up with a state machine that's more complex than the pattern itself. In those cases, consider a proposal system with explicit approval stages rather than an offer pattern. Real-time dynamic pricing is the second failure mode. If your business model depends on prices changing every few minutes based on demand — ride sharing, airline tickets, wholesale commodities — an offer with a fixed price is either impossible to sustain or requires constant repricing that defeats the purpose. These systems need auction-style or negotiation-style patterns instead.

Extremely high volume with short TTLs is the third. If you're generating millions of offers per day with a five-minute expiry and need to clean them up constantly, the overhead of writes, reads, and TTL management becomes significant. I've seen teams migrate away from this pattern to a simpler cart-then-pricing-at-checkout model when the volume hit that scale. The pattern isn't wrong — it's just expensive at that scale. There's also a subtle operational cost most teams don't plan for: monitoring. You need alerts when the expired-but-not-cleared offer count grows unexpectedly, because a stuck background job will silently let offers accumulate until your Redis or database starts feeling it. I set up a simple daily check that flags any offers still in draft status past their expiry window, and it's caught more than one deployment issue over the years.

A Note on Terminology

When you search for documentation on the Offer Letter Pattern, you'll find scattered references. It's not formalized in the usual pattern catalogs. The closest established concepts are the Quote Pattern and the Reservation Pattern. If you're looking for reference implementations, start with how Shopify handles quotes and how Stripe handles payment intents with scoped pricing. Both use similar snapshot-and-lifecycle mechanics, though neither calls it the same thing.

Free Job Offer Letter Templates, Editable and Printable - Free to learn
Free Job Offer Letter Templates, Editable and Printable - Free to learn