Shipt startet Kits-Plattform für thematische Lebensmittelbündel
Mit der Kits-Funktion lassen sich beim Lieferdienst Shipt schnell Warenkörbe für bestimmte Anlässe zusammenstellen. Die Architektur mit Items, Einkaufslisten und Kits ermöglicht es Merchandisern, store-spezifische Kits ohne aufwendige manuelle Arbeit anzulegen.

InfoQ Homepage Articles Composable Commerce: Inside Shipt’s Kit Platform
In June 2026, Shipt launched Shipt Kits, a one-click cart-building feature designed to help customers plan their hosting events. Timed around a global soccer event, the company’s national brand campaign introduced Shipt Kits through a celebrity partnership, highlighting summer use cases such as watch parties, backyard hosting, and cookouts. Customers landing on a "Match of the Week" kit could add a full evening’s worth of groceries – sides, drinks, snacks, dessert – to their cart in one click. Each item was store-aware: the kit you saw at your store reflected what was actually on shelves near you, not a generic catalog.
The campaign was covered in Progressive Grocer, Supermarket News, ¡HOLA!, and Better Homes and Gardens. From the outside, it looked like a polished new feature.
From the inside, the team had three weeks to build it. The API was done in ten days. The remaining eleven went to the admin experience, the customer surfaces, and the operational hardening that turns a working prototype into something a merchandising team can use without supervision.
This is an architecture tour of what was built in those three weeks: how the platform replaced a multi-day clerical workflow with a single admin UI, how it was modeled as three first-class entities that compose well, how the catalog was made store-aware end-to-end, and how it was made safe to edit live without a full draft-publish lifecycle.
Figure 1. Platform architecture overview (created by author)
The Kit API sits between the merchandiser-facing admin UI and the entity layer, with store-aware resolution and kit-type compliance rules determining what a customer ultimately sees, and edit-safety guards protecting in-flight changes.
Before Kits, building a themed shopping experience at Shipt looked like this. A merchandiser who wanted to assemble, say, a Tailgate Bundle would:
After all that work, the bundle still wasn’t guaranteed to display correctly at every store. The workflow was pegged on product ID, not on per-store inventory, so a kit that looked complete in the admin tool could be missing half its items at the customer’s actual store. The merchandiser had no way to know in advance.
Figure 2. The pre-Kits workflow: six manual steps and no per-store guarantee (created by author)
For a one-off promotion that lived for a week, this was tolerable. For a national campaign that needed dozens of fresh kits launching on cadence – across multiple retailers, multiple regions, multiple weeks – it was impossible. When the campaign got the green light with a fixed launch date, the team had three weeks to build a system where merchandisers could describe a bundle once and have the platform handle the per-store, per-retailer materialization.
The first architectural decision was the most important: model the platform as three composable entities, not one monolithic "kit" type.
The approach draws on the design vocabulary of Domain-Driven Design (DDD). Items, shopping lists, and kits form the ubiquitous language shared across code, admin UI, and merchandiser conversations, and each is modeled as an aggregate that owns its own invariants. Kits act as aggregate roots for editorial and structural metadata; items live in their own bounded context and are shared by reference rather than duplicated inside every kit that uses them. Naming these boundaries early kept downstream feature work – new kit types, ad monetization, per-store resolution – additive rather than disruptive.
Items are the atomic catalog units – generic, typed, and tagged. An item knows what kind of thing it is (a product, an ingredient, a non-product element of an editorial bundle) and carries arbitrary metadata in a JSON tag bag. Items are not retailer-specific; the resolution to a particular store’s product ID happens at view time, not at modeling time.
Shopping lists are curated sequences of items. A shopping list can be merchandiser-curated and attached to a kit (the "here’s what to grab for your party" list inside a Game Day Bundle) or customer-built (a personal grocery list that the customer maintains across sessions). The same shopping-list abstraction supports both, which means the platform maintains one mental model and one set of APIs rather than two parallel ones.
Kits are the composable bundles – the things customers see, click, and add to cart. A kit carries editorial metadata (title, description, hero image, theme), structural metadata (slot layout, type, anchor items), and references to items and optionally to shopping lists.
Figure 3. Items as a single source of truth, referenced by both shopping lists and kits (created by author)
The platform’s most consequential property is that items are shared by reference across shopping lists and kits. The same Heinz ketchup item powers the Tailgate Bundle, the Backyard BBQ Bundle, the Family Picnic Bundle, the customer’s personal grocery list, and twenty other surfaces. Updating the item’s metadata propagates everywhere it’s used. Adding a new retailer mapping for that item lights it up across every kit and list that references it, automatically.
{ "id": "itm_01H2N7X...", "item_type": "product", "canonical_name": "Heinz Tomato Ketchup, 20 oz", "tags": { "category": "condiments", "dietary": ["vegetarian", "gluten-free"], "seasonal": ["summer", "grilling"], "brand_id": "brand_heinz" } } The tags field is intentionally a schemaless JSON bag – new dimensions (dietary, seasonal, promotional, anything a merchandiser or downstream service wants to filter on) can be added without a migration. Retailer-specific product IDs deliberately don’t live on the item; they’re resolved at view time, per store, which is the subject of the next section.
The migration that unified items into this single source of truth – kit products and shopping-list items both pointing at the same item rows – was mechanically boring but paid off immediately. Maintenance dropped. New kit types became inexpensive to introduce because a new type just needed to know which items it references, not how to manage its own private item table.
A kit is more than a bag of items. Some items are essential to the kit’s identity. A summer grilling kit without meat is not a summer grilling kit. A pasta-night kit without pasta is not a pasta-night kit. An alcohol kit without alcohol is, legally and conceptually, something else.
The team modeled this with the concept of anchor items. An anchor item is one whose absence – at a specific store, on a specific day – disqualifies the entire kit from being shown at that store. The merchandiser marks anchor items during kit creation. The resolution layer checks anchor availability before deciding whether to surface the kit.
Anchor items also drive a per-kit minimum product threshold. A kit can declare that it needs at least, say, eight non-anchor items available at the customer’s store to be worth showing, in addition to all anchor items being present. The thresholds and anchors compose: a kit is shown only if (a) every anchor item resolves at the store and (b) the total available item count clears the threshold. As a result, the platform never shows customers a half-empty kit. The kit either makes sense at this store today, or it doesn’t appear in the customer’s catalog at all. For a curated, editorial product, this is the contract the team wants.
Figure 4. The customer’s effective retailer set is the intersection of three independently configured layers (created by author)
Behind every customer view of a kit is a small set-theoretic computation that decides which retailers’ inventories to query. A customer at a given zip code may be served by a set of grocery retailers–the customer’s local Hy-Vee, Target, Meijer, and Giant Eagle. Each kit has its own allow-list of retailers it’s enabled for. Some kits–alcohol kits, most notably– also have additional eligibility constraints that depend on store-level licensing.
The resolution is the intersection of all three:
effective_retailers = customer_zip_retailers ∩ kit_allowed_retailers ∩ kit_eligibility(store) For an alcohol kit, the eligibility check filters to stores with active alcohol licenses in the customer’s jurisdiction. For a kit with no special eligibility, the third set is unconstrained. For a general national-campaign kit, the allow-list covers Shipt’s operational areas. Each layer is independently configurable; the intersection is computed very efficiently at request time.
Once the effective retailer set is known, the platform fans out concurrent searches against each retailer’s catalog, collects the results, and assembles the customer-facing kit. Items are scored against the retailer’s actual inventory, anchor items are checked, the minimum threshold is verified, and the kit is either rendered or quietly dropped from the customer’s view.
The fan-out matters operationally. A customer at a metro zip might be eligible for four retailers; the kit needs to materialize quickly enough to feel instantaneous. Sequential per-retailer queries would have been too slow at the latency budgets the team was targeting. Concurrent fan-out, with per-retailer timeouts and graceful degradation, keeps the customer-perceived latency flat regardless of how many retailers the customer is eligible for.
Each retailer call runs independently and in parallel, with its own fixed timeout rather than a single timeout for the whole fan-out. If a retailer doesn’t respond within that window, its result is simply omitted from that customer’s kit view rather than blocking or failing the request. This keeps customer-perceived latency bounded by a fixed ceiling regardless of how many retailers the customer is eligible for, rather than growing with the number of retailers or stalling on a single slow one.
Figure 5. Concurrent fan-out for retailer calls (created by author)
Alcohol is the clearest example of why the team settled on kit types as an abstraction.
A generic kit’s lifecycle is editorial: a merchandiser creates it, sets its theme and items, publishes it. An alcohol kit’s lifecycle is editorial plus compliance: the same creation flow plus a separate store-eligibility check (does this store hold an active alcohol license?), plus product-level filtering (only products classified as alcoholic, no mixers-as-alcohol false positives), plus jurisdictional constraints (an alcohol kit is hidden at the customer level if the customer’s zip falls outside the retailer’s licensed delivery area).
We made all of that part of the alcohol kit type rather than special-casing the generic kit. The type carries its own eligibility predicates; the merchandiser doesn’t need to remember to apply alcohol rules because the type applies them automatically. New compliance constraints can be added by extending the type’s predicate without touching the generic kit code path.
The team expects more of these – pharmacy, age-restricted content, regulated brands. Each one is a good candidate for its own kit type rather than a new flag on the generic one.
The first revenue surface on top of the platform is onsite ad monetization. A consumer-packaged-goods brand pays to be featured prominently inside a kit – their product appears in a designated slot, branded, sometimes with promotional pricing.
The mechanics require a brand registry, sponsor associations on a per-kit basis, and an answer to a question the team hit almost immediately: what happens when the sponsored brand’s product isn’t available at the customer’s store?
Neither of the obvious answers works cleanly. Showing the kit without the sponsored item breaks the sponsorship agreement – the brand paid for placement, and silently dropping it leaves the customer with an awkward gap. Hiding the entire kit is worse in the other direction – one out-of-stock sponsored beverage shouldn’t sink a whole Game Day Bundle for the customer.
The behavior is contractual, so we made it configurable per kit. A brand_fallback field takes one of two values: hide item (drop the sponsored item, render the rest of the kit) or hide kit (don’t show the kit at this store). The merchandiser picks the value that matches the brand’s contract at kit-setup time, and the resolution layer enforces it at view time.
The brand registry itself is synced from Shipt’s upstream catalog system on a cadence. The sync is streaming and idempotent: it pages through the upstream catalog, upserts into the local brand table, and writes only where values would actually change. Re-running the sync after a partial failure is safe – already-correct rows are no-ops, and inconsistent ones converge on the next run.
Merchandisers edit kits in a live system. Customer carts and placed orders are snapshots taken at add-to-cart and order time, so live edits don’t rewrite history – but the kit itself is mutable, and every save is visible to the next customer. The textbook fix is a full draft-publish lifecycle (draft rows, atomic publish, diff UI, snapshots), at two to four extra weeks of build.
That time wasn’t available in the launch window, so the team shipped four guards instead:
Together, the four layers cover roughly 70% of what a true draft-publish system would give the platform, at maybe 10% of the cost – enough to ship the campaign safely, with the full lifecycle now visible on the roadmap.
The next three investments are clear, and they’re investments rather than rework – the platform is structured to accept them.
Layered guards are a good 70%. Draft-publish is 100%. The remaining 30% shows up in workflows we don’t currently support well: a merchandiser editing a kit over multiple days, an editorial reviewer approving changes before they go live, an audit trail of who changed what when, automatic rollback when a published change causes a regression.
Since the campaign, we’ve taken the first concrete step – a Testing status that puts a kit live in production but only visible to a global tester allowlist, so internal reviewers see the real customer surface before real customers do. The lifecycle is now a genuine pipeline (Draft → Testing → Published → Archived) rather than a binary switch; per-field drafts, atomic publish, diff UI, and audit trail are the next slice.
The ad monetization layer ships placements today. The natural next step is closed-loop attribution: a brand pays to be featured, and we report back attributed revenue at the kit, item-within-kit, and impression levels. This is plumbing more than research – the events exist, the joins are tractable, the reporting surface is the work.
Today’s resolution layer is per-store. The next axis is per-customer: a customer with a "no pork" or "gluten-free" preference should see a Tailgate Bundle that respects it. The mechanics are an additional intersection in the resolution layer, alongside the per-store ones already in place. Anchor items and minimum thresholds will need to be re-evaluated per customer rather than per store, which is a real but contained change.
Three things would have saved me time if I’d known to look for them earlier:
Shipt Kits has been live for a few weeks now, handling real merchandiser workflows and real customer traffic across the guardrails, resolution, and compliance layers described above. The team is now planning its next investments – draft-publish, ROAS attribution, per-customer filtering – from a base of real usage rather than assumptions.
The specifics above are about grocery kits, but the underlying modeling questions are not unique to grocery, or even to commerce. Any platform that lets customers combine multiple things into one purchasable or consumable unit – travel packages, subscription boxes, meal kits, ticket bundles, curated content collections – runs into the same handful of decisions: how many first-class entities to introduce before the system becomes unmanageable; how to resolve eligibility when several independent constraints apply at once, such as location, inventory, regulation, and pricing; how to give a bundled offering an identity that customers can trust rather than one that silently falls apart; and how to support regulated or exception-heavy categories without littering the core model with special cases.
None of these questions has a universal right answer. The right level of abstraction depends on how often new constraints or categories will show up, and how much operational cost a team can absorb up front versus later. What held true in this case, and what tends to hold true elsewhere, is that the answer rarely comes from more process or more documentation. It comes from naming the entities and constraints explicitly in the model, early enough that the system can grow by composition instead of by exception.
Thanks to Marc Kapke (Senior Principal Engineer, Shipt) for review and technical feedback, and to the Shipt Marketplace team for the collaboration that made this platform possible.
Equal Experts helps organizations harness AI, modernize technology, and build better software through expert engineering, collaboration, and innovative delivery practices. Learn more.