Skip to content
AG Systemsbrief → productionContact

InsightsProduct engineering

Availability is a decision, not a property

A product can be published, discoverable, offered, and fulfillable on different terms. Store those facts separately, let placements combine rules with curation, and resolve the customer-facing answer in context.

Author
Alex GavriscoFounder & Principal Software Engineer
Published
Updated
Length
9 min read

basis: Draws on availability and merchandising questions from a recent commerce engagement.

An operator combines product representation, offer, fulfilment, placement, and request context into an availability decision that branches into several customer-facing surfaces
illustration: product facts and merchandising intent converge on a contextual customer-facing decision
The requirement

The same product can have several correct answers

A limited release can be visible on its own page, absent from the current catalog, retained in an archive, pinned to the homepage, and unavailable for purchase. None of those statements conflicts with the others. They answer different questions.

“Is this product available?” compresses those questions into one word. The useful version is more precise: available to whom, in which channel, for what action, in what quantity, and at what time?

Availability is therefore not a property carried by the product. It is a decision made from several independent facts and a request context. The model should preserve those facts. The storefront should receive the decision, together with the reasons behind it.

Domain model

Store facts and intent, not one final answer

The product supplies stable identity: what the item is and which history belongs to it. Four surrounding concerns change for different reasons and at different rates.

ConcernOwnsAnswers
RepresentationThe product information customers may seeWhich approved version can this channel present?
OfferCommercial intent, price, channel, and validityIs the business offering it here and now?
FulfilmentAvailable-to-promise quantity and policyCan this quantity be supplied to this destination?
PlacementSurface membership and orderingWhere should customers discover it?
fig. 1: independent facts used to answer availability

Representation is more useful than a visibility switch. It distinguishes work in progress from the approved version exposed to a channel. Ending an offer does not unpublish that representation, so a past release can keep its photography, editorial copy, and permanent URL without pretending it is still for sale.

This separation appears in larger commerce systems for the same reason. Saleor distinguishes publication, visibility in listings, and availability for purchase, while noting that purchase availability does not guarantee stock. commercetools exposes a customer-facing product projection selected from current or staged data and shaped by store, price, locale, and inventory context. See the Saleor channel-listing model ↗ and commercetools product projections ↗.

A smaller catalog does not need every mechanism those platforms provide. It benefits from the same boundary: editorial publication, commercial intent, fulfilment, and merchandising should not overwrite one another.

Contextual answer

Resolve availability for a particular request

A useful resolver accepts the product and the context that can change the answer: channel, market, destination, requested quantity, customer eligibility, and time. Its output is not another all-purpose status. It is a set of explicit decisions.

availability(product, context) = {
  representation: PUBLISHED,
  offer: ACTIVE,
  fulfilment: UNAVAILABLE,
  purchasable: false,
  reasons: [insufficient_inventory],
  placements: [
    catalog,
    homepage_feature
  ]
}

Here, the business still offers the product and customers may discover it, but the requested quantity cannot be fulfilled. The page can show a sold-out state without removing the product from the catalog or erasing a deliberate homepage selection.

The distinctions also make different consistency requirements visible. A published description may tolerate the storefront’s publication delay. A checkout decision about the last unit may not. Both consume the same domain facts, but they are resolved at different boundaries.

Merchandising

Placements are policies, not flags

A featured flag answers one question for one surface. The next campaign adds another flag, and soon the product carries a list of presentation decisions that belong to the storefront rather than to the product.

A placement is a named merchandising surface. It owns how products enter that surface, which candidates remain eligible, and how the results are ordered. Membership can be derived, curated, or hybrid.

  • Derived placements select products from domain facts. The current catalog and archive are natural examples.
  • Curated placements contain explicit operator choices. A homepage feature belongs here.
  • Hybrid placements begin with a rule and accept pins, exclusions, or ordering overrides.

Shopify makes a similar distinction between automated and manually managed collections, with publication and sorting handled separately. That precedent is useful, but the placement concept can remain much smaller than a general collection engine. See Shopify’s collection model ↗ and publication model ↗.

SurfaceMembershipEligibilityOrdering
Current catalogDerived from a public representation and an active offerPublic and offeredOperator order, then release date
ArchiveDerived when the public representation remains but the offer endsPublicRelease date, newest first
Homepage featureCurated explicitly by an operatorDefined by the surface policyManual
New arrivalsRecent releases, with explicit pins and exclusionsPublic and offeredPins first, then release date
fig. 2: one placement abstraction, several membership strategies

Eligibility is a guard, not a cleanup job. If an operator features a product and its representation is later withdrawn, the placement entry can remain while the public resolver suppresses it. The administration view can say why it is absent. Restore eligibility and the original curation returns without asking the operator to remember it.

This matters because deletion destroys intent. A placement entry says, “show this product here when the surface permits it.” Product and offer state decide whether that instruction can currently be honoured.

Implementation

Store the domain; project the storefront

The storefront does not need the full write model. It needs a projection containing the approved representation, current offer, customer-safe fulfilment result, and resolved placements. A relational database can produce that view in a query for a small catalog. If read volume or publication cost later justifies it, the same output can be materialised and rebuilt from its sources.

The storage model might map the concepts to relations like these:

product(id, ...)
product_revision(id, product_id, state, ...)
publication(product_id, channel, revision_id, published_at)
offer(id, product_id, channel, state, valid_from, valid_until, price)
inventory_position(sku, location, on_hand, reserved, safety_stock)
placement(id, key, membership_policy,
    eligibility_policy, ordering_policy)
placement_entry(placement_id, product_id, intent,
    rank, starts_at, ends_at)

These are implementation choices, not the domain model itself. A first release may keep one working revision and one published revision, one sales channel, and a short list of policy names defined in code. It does not need a user-authored rules language or a separate service for every concern.

What matters is authority. Product edits change the representation, and each surface resolves from that source instead of keeping its own copy. An offer may close while the product’s editorial history stays intact. A placement can influence discovery, but it cannot grant permission to buy. The derived read model contains none of those decisions, so it can be discarded and rebuilt without losing them.

Final authority

The catalog informs; checkout decides

Raw stock is not the quantity available to a customer. The useful number is available to promise: what remains after reservations, safety stock, location constraints, and the applicable fulfilment policy. Shopify distinguishes on-hand, available, committed, reserved, and safety-stock quantities; Medusa likewise calculates availability from stocked and reserved quantities at locations linked to a sales channel. See the Shopify inventory states ↗ and Medusa reservation lifecycle ↗.

A product page can display a recent fulfilment result, but checkout must evaluate the requested quantity again. The database first records the pending order and conditionally reserves the units. Payment then runs as an external step under an idempotency key. A successful payment finalises the order; a failed or abandoned attempt releases the reservation.

Reservation and payment still do not form one atomic transaction. The order therefore keeps an inspectable state, and reconciliation handles the cases where one side succeeds while the other cannot be confirmed. Stripe recommends one PaymentIntent per order or customer session and supports idempotency keys so a request can be retried without creating a second operation. See Stripe’s PaymentIntent model ↗ and idempotent-request contract ↗.

Operations

Derived decisions need explanations

When several rules contribute to availability, a bare false reports only the outcome. It tells neither the storefront what to explain nor an operator which condition prevented purchase. The resolver should return stable reason codes that the storefront can translate for customers and the administration view can expand for operators.

  • not_publishedNo public representation exists for this channel.
  • offer_suspendedThe offer is temporarily unavailable for purchase.
  • offer_not_startedThe commercial window has not opened.
  • offer_endedThe product remains public, but its offer has ended.
  • insufficient_inventoryThe requested quantity cannot be promised.
  • destination_unsupportedNo eligible fulfilment route serves the destination.

The same trace should explain placement membership: which rule selected a product, which operator override changed its position, and which guard is suppressing it now. That turns a flexible model into one an operator can trust.

Deliberate limits

Keep the boundaries; resize the machinery

Clear concepts do not require a large platform. A compact implementation can use one database, one application, named placement policies, and a rebuildable storefront projection. Scheduled publication, versioned catalogs, multiple markets, variant-level offers, back orders, and warehouse selection can remain outside the first release until the business needs them.

The useful investment is not maximum configurability. It is preserving the distinctions that are expensive to recover later: public content is not an offer, an offer is not inventory, inventory is not placement, and none of them alone answers whether a customer can complete a purchase.

Reusable exercise

Replace “available” with a decision

  1. Which representation may this audience see?
  2. May they discover it on this surface?
  3. Is there an active offer for their channel and time?
  4. Can the requested quantity be promised to their destination?
  5. Which placement policy selected it, and how was its position chosen?
  6. If the answer is no, can the system explain which boundary refused it?

Answer those questions independently, then compose them for the storefront and re-evaluate the commercial promise at checkout. The resulting model is easier to extend because each new rule has somewhere honest to belong.

See the decision in the full engagement record →Read how catalog publication was operated →

Discuss a system

Working through a similar decision?

Describe the system, the constraint, and the options on the table. Alex will review the context and suggest a practical next step.