A catalogue is a query
There is a shop inside our platform. Partners order desk phones, headsets and DECT bases; they order phone numbers; they add contract addons to existing services. It looks like e-commerce and it is nearly nothing like e-commerce, for one reason: there is no such thing as the catalogue.
What can be ordered depends on who is asking, what they already have, what contract they are on, and how much of that contract is left. Two partners looking at the same page legitimately see different products at different prices with different available terms — and neither view is the real one.
Availability is per-partner
Resellers sell our platform as their own, and they do not all sell the same thing. Some are configured to offer a subset of hardware; some deliberately do not offer certain models because they do not want to support them; some have commercial arrangements that make one manufacturer preferable.
So orderable hardware is restrictable per reseller, which means every catalogue question is scoped to a viewer. That sounds like a filter and it is really a projection: you cannot cache "the product list", because there isn't one. You can cache a product list for a partner, and now cache invalidation has a new dimension.
The same is true of the presentation layer — notes, images, descriptions and defaults per store — which means the shop is not one shop rendered with a theme, it is many shops sharing an engine.
Pricing is a period, not a number
The thing I would tell anyone building this: prices are not values on products, they are values with validity windows, and you need to ask explicitly whether current pricing exists at all.
That last part is not defensive programming. A product can be listed, real, and orderable in principle while having no valid price right now — because a pricing period lapsed, a new one has not been published, or a variation was added without commercial sign-off. The honest answer to "how much is this?" is sometimes "there is no current price", and a system that assumes a price always exists will confidently show a wrong one or a zero.
Products also have variations — the same handset in different configurations — and pricing attaches to the variation, not the product. Conflating them is the kind of mistake that is cheap to fix on day one and involves a data migration on day four hundred.
Addons have to fit inside the contract
This is the constraint that makes the domain genuinely interesting rather than merely fiddly.
An addon bought against an existing service has a term. So does the service. If a customer is eighteen months into a three-year contract and orders an addon with a two-year term, the addon outlives the thing it is attached to. That is not a validation error in any obvious sense — both objects are individually valid — but it is commercially incoherent and somebody will eventually have to unpick it by hand.
Term matching means the ordering system has to understand the customer's existing commitments well enough to constrain what it offers. The catalogue query now depends not just on who is asking but on the remaining life of what they already bought.
You cannot bolt this on at checkout. By the time someone has chosen, it is too late to tell them the thing they chose was never available to them — you have to not offer it.
An order is a workflow, not a transaction
When an order is placed, several different things begin, on different timescales, with different failure modes.
Hardware has to be picked and shipped, physically, by people. Numbers have to be allocated from a range and configured. Service has to be provisioned. Billing has to be set up against the right contract terms. Somebody in operations may need to do something manually, so a ticket is raised with the order details attached.
None of this is atomic and it should not pretend to be. Hardware can be out of stock while numbers allocate fine. A number range can be exhausted while the handset ships tomorrow. The order is a long-lived object with per-item progress, and the customer needs to see that honestly rather than watching a spinner that means four different things.
We publish order events onto a message bus so the downstream pieces can proceed independently — which is the right shape, and it means the order's own record of truth has to be assembled from things that completed at different times.
The partner is not the customer
One recurring subtlety: there are two parties on every order and they need different things.
The end customer receives the hardware and uses the service. The partner is who we have the commercial relationship with, who gets billed, and who needs to be contactable when something goes wrong. Order records carry both, and getting them the wrong way round produces the sort of error where a delivery notification goes to the wrong company entirely.
It also means "where does this go?" and "who pays for this?" are separate questions with separate answers, and the data model has to keep them apart even though on most orders they point at the same place.
What I took from it
A catalogue is a query, not a table. As soon as availability depends on the viewer, stop thinking of it as a list you filter and start thinking of it as a question you answer per request. It changes how you cache, how you test, and what "the product list" even means.
Prices need validity, and "no price" is a valid answer. Model pricing as periods from the start, and make the absence of a current price a state the system can express rather than a case it falls over on.
Constrain at selection, not at checkout. Any rule about what somebody is allowed to buy has to shape the options, not reject the choice. Rejecting late is a terrible experience and usually means the rule lives in the wrong layer.
Orders are long-lived objects with partial progress. Anything involving physical goods, external allocation and human steps will be partially complete for days. Designing that as a transaction with a status field is the mistake you pay for in support tickets.
Also in this series
- The tool your colleagues live in — maintaining the portal a company runs on
- One person, several accounts — SSO when identity isn't one-to-one
- Hiding a button is not a permission — retrofitting granular access control
- Reports are a scheduling problem — building call reporting end to end
- Decommissioning is the milestone — replacing the helpdesk support lives in