Customer-specific pricing: choose the source of truth before the cache
Build a pricing cache key from a named context containing the account, Store, product, quantity, unit, currency, contract, and policy revision.
In this guide
- Authorized buying context
- Price authority
- Validated result
- Scoped cache
- Checkout validation
The fast response with the wrong contract price
Two buyers request the same product. The first belongs to a service account with negotiated pricing; the second buys under a different agreement. A cache keyed only by SKU returns quickly for both. It has improved response time while violating the most important requirement of the pricing integration.
Before choosing a cache, identify the price authority. Does Znode own the account price list, does an ERP calculate the sell price, or does the commerce workflow combine several documented sources? A cache should preserve that decision. It must not become an accidental fourth pricing system with its own undocumented fallback rules.
Describe the complete pricing question
A price may depend on account, store, SKU, quantity, unit of measure, currency, effective date, contract, or delivery conditions. Not every implementation uses every dimension. Determine the actual dependencies from the source contract and commercial rules rather than copying a long generic key.
Resolve the account and store through trusted context. A browser-supplied account number cannot establish access to its prices. If a pricing request spans products or accounts, keep each result associated with the exact input that produced it. A response with a matching SKU but a different currency or account is not a valid cache candidate.
Build an unambiguous cache key
Build a PriceCacheContext from the authorized pricing request. Its named properties identify policy revision, Store, account, SKU, quantity, unit of measure, currency, and contract. PriceCacheKey.Build requires each string dimension and a positive quantity, then returns a deterministic string key.
The helper prefixes each value with its length, so separators inside an identifier cannot make two different input sequences look identical. Quantity uses invariant decimal formatting, so 1m and 1.00m produce the same quantity text. Identifier case is preserved; normalize it only when your pricing contract defines equivalent forms.
Pricing/PriceCacheKey.cs csharp
using System;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Gcg.DeveloperExamples.Pricing
{
public sealed class PriceCacheContext
{
public string PolicyRevision { get; set; }
public string StoreCode { get; set; }
public string AccountCode { get; set; }
public string Sku { get; set; }
public decimal Quantity { get; set; }
public string UnitOfMeasure { get; set; }
public string Currency { get; set; }
public string ContractCode { get; set; }
}
public static class PriceCacheKey
{
public static string Build(PriceCacheContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
string[] fields =
{
context.PolicyRevision, context.StoreCode, context.AccountCode,
context.Sku, context.UnitOfMeasure, context.Currency, context.ContractCode
};
if (fields.Any(string.IsNullOrWhiteSpace) || fields.Any(field => field.Length > 200))
throw new ArgumentException("Every pricing dimension must be present and bounded.");
if (context.Quantity <= 0)
throw new ArgumentOutOfRangeException(nameof(context.Quantity));
// Prefix each value with its length to prevent separator collisions.
var key = new StringBuilder();
foreach (string field in fields)
AppendField(key, field);
string quantity = context.Quantity.ToString("G29", CultureInfo.InvariantCulture);
AppendField(key, quantity);
return key.ToString();
}
private static void AppendField(StringBuilder key, string value)
{
key.Append(value.Length.ToString(CultureInfo.InvariantCulture))
.Append(':')
.Append(value);
}
}
}
The value 'USD' is encoded as '3:USD'. The supplemental call site shows the full PriceCacheContext initializer and proves that equivalent quantities share a key while ACCOUNT-A and ACCOUNT-B do not. If effective date, locale, or another input changes the price, add that dimension explicitly or encode it in a documented revision value before using this key.
Choose a failure policy explicitly
An expired entry is a business decision, not merely a cache miss. Decide whether a stale price may be displayed, whether it must be labeled provisional, and whether checkout requires revalidation. A browsing fallback can be acceptable while the same fallback at order acceptance would be incorrect.
Do not substitute a generic account when context is missing. Do not treat an unavailable provider as a zero price. Distinguish no applicable price, authorization failure, malformed response, and dependency failure. Batch requests where the verified API supports it, bound timeouts, and avoid launching an unbounded external call for every product tile.
Use distinctive prices to prove isolation
Create two fictional accounts with conspicuously different prices for the same SKU. Vary quantity, currency, and store where those dimensions apply. Request them in alternating order and concurrently, then switch accounts in the browser. This makes accidental reuse visible instead of hiding it behind nearly identical values.
Advance the effective date, expire an entry, change a contract revision, and simulate a provider outage. Verify the product page, cart, and final calculation independently. A correct cache lookup on a product listing does not establish the checkout price. Inspect persisted order values after a controlled test when the target environment permits it.
- Check negative-result caching separately from successful prices.
- Avoid logging complete account and product payloads.
- Measure hit rate together with correctness and stale-result age.
Keep the cache subordinate
Document where the cached price can be displayed and where the authoritative calculation must run again. Give agreement changes and policy revisions an explicit invalidation owner. Keep the key out of ordinary logs because it contains account and commercial identifiers.
The key builder does not choose the source of truth, cache lifetime, or outage fallback. Those decisions belong to the pricing policy. Once they are defined, measure cache hit rate together with price correctness and the age of returned values.
Build a cache key from canonical pricing dimensions
Compile this call site beside PriceCacheKey. ExampleContext constructs a PriceCacheContext with named StoreCode, AccountCode, Sku, Quantity, UnitOfMeasure, Currency, ContractCode, and PolicyRevision values. Example checks that 1.00m and 1m share a key and that a different account gets a different key. In an application, resolve these values from authorized pricing context and keep the generated key out of routine logs.
Pricing/PriceCacheKeyUsage.cs csharp
using System;
using Gcg.DeveloperExamples.Pricing;
public static class PriceCacheKeyUsage
{
public static string Example()
{
string first = PriceCacheKey.Build(ExampleContext("ACCOUNT-A", 1.00m));
string sameQuantity = PriceCacheKey.Build(ExampleContext("ACCOUNT-A", 1m));
string otherAccount = PriceCacheKey.Build(ExampleContext("ACCOUNT-B", 1m));
if (first != sameQuantity)
throw new InvalidOperationException("Equivalent quantities must share a key.");
if (first == otherAccount)
throw new InvalidOperationException("Different accounts must remain isolated.");
return first;
}
private static PriceCacheContext ExampleContext(string accountCode, decimal quantity)
{
return new PriceCacheContext
{
PolicyRevision = "policy-2",
StoreCode = "STORE-A",
AccountCode = accountCode,
Sku = "EXAMPLE-VALVE",
Quantity = quantity,
UnitOfMeasure = "EA",
Currency = "USD",
ContractCode = "CONTRACT-A"
};
}
}
References and further reading
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.