Repo-Specific Refactor Blueprint¶
This document maps the target commerce architecture onto the repos that already exist in the workspace. It is intentionally pragmatic: it starts from the code that is already there and shows what each repo should own, what should move, and what should become a read model instead of a source of truth.
Current Ownership¶
catalog-service¶
Owns today:
- catalog CRUD
- search
- promotions and coupons
- import and export
- ingestion routes
- inventory mutations
- inventory event publishing and consumption
Implication:
- this repo is already the best place to anchor canonical product metadata
- it is also the current inventory owner, even though some schema and route layers are inconsistent between SQLite-era code and newer Postgres migrations
store-hub¶
Owns today:
- stores
- terminals
- staff
- customers
- locations
- reports
- dark store operations
- dark store orders and ETA logic
Implication:
- this repo should remain the owner of operational store and dark-store state
- it should not be the canonical owner of stock truth or pricing truth
api-gateway¶
Owns today:
- auth and tenant middleware
- rate limiting
- proxy routing
- upstream health checks
Implication:
- this repo is a secured edge and routing layer
- it should gain a small number of composed BFF routes, but should not absorb domain logic that belongs in business services
sync-service¶
Owns today:
- terminal registration
- push and pull sync
- conflict handling
- queue processing for offline changes
- consumption of
catalog.events
Implication:
- this repo should stay focused on terminal and offline replication
- it should not become the owner of catalog or inventory business state
retail-agent¶
Owns today:
- auth
- releases
- incidents
- webhooks
- billing integration
- catalog enrichment
- terminal management placeholder APIs
Implication:
- this repo is a control plane
- LLM-powered enrichment belongs here
- terminal source-of-truth should still live elsewhere
Target Ownership¶
catalog-service should own¶
- canonical product metadata
- canonical variant metadata
- source-system mappings
- canonical prices
- canonical per-location stock positions
- stock movement ledger
- inventory reservations
- ingestion normalization
- catalog and inventory domain events
store-hub should own¶
- stores
- locations
- terminals assigned to stores
- staff and shifts
- dark-store metadata
- dark-store zones
- dark-store pickers
- dark-store order workflow
- dark-store metrics
- dark-store assortment and inventory projections for fast local reads
api-gateway should own¶
- authentication and policy enforcement
- route proxying
- a thin BFF for store-scoped catalog views
sync-service should own¶
- terminal sync sessions
- offline queues
- conflict policies
- replay and reconciliation
retail-agent should own¶
- AI enrichment
- release control
- incident control
- outbound webhook delivery
- control-plane automation
Keep, Migrate, Deprecate¶
Keep in catalog-service¶
categoriesproductsvariants- search index maintenance
- ingestion routes and normalization pipeline
Add to catalog-service¶
- source-system tables
- version history tables
- normalized price tables
- canonical stock ledger tables
- reservation tables
- orderability projection tables
Deprecate in catalog-service¶
- SQLite-style inventory tables as the long-term truth model
- SQLite-style
inventory_adjustmentsas the final ledger model - JSON
price_lists.entriesas the primary price storage model
Keep in store-hub¶
storeslocationsterminalsdark_storesdark_store_zonesdark_store_pickersdark_store_ordersdark_store_order_itemsdark_store_metrics
Deprecate as authoritative in store-hub¶
inventory_by_locationdark_store_skus.quantity_on_handdark_store_skus.unit_pricedark_store_skus.unit_cost
These can remain temporarily as projections while downstream code is migrated.
Canonical Schema¶
1. Catalog Core in catalog-service¶
Keep existing categories, products, and variants from the Postgres
schema, then add the following tables.
CREATE TABLE IF NOT EXISTS source_systems (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
source_type VARCHAR(40) NOT NULL,
name VARCHAR(120) NOT NULL,
vendor VARCHAR(80),
auth_config JSONB NOT NULL DEFAULT '{}',
webhook_secret TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS source_entities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
source_system_id UUID NOT NULL REFERENCES source_systems(id) ON DELETE CASCADE,
entity_type VARCHAR(40) NOT NULL,
external_id VARCHAR(200) NOT NULL,
canonical_type VARCHAR(40) NOT NULL,
canonical_id UUID NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, source_system_id, entity_type, external_id)
);
CREATE TABLE IF NOT EXISTS catalog_ingestion_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
source_system_id UUID REFERENCES source_systems(id) ON DELETE SET NULL,
ingestion_type VARCHAR(40) NOT NULL,
status VARCHAR(30) NOT NULL,
received_count INTEGER NOT NULL DEFAULT 0,
success_count INTEGER NOT NULL DEFAULT 0,
failed_count INTEGER NOT NULL DEFAULT 0,
error_summary JSONB NOT NULL DEFAULT '[]',
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS catalog_item_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
variant_id UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
version_no INTEGER NOT NULL,
source_system_id UUID REFERENCES source_systems(id) ON DELETE SET NULL,
payload JSONB NOT NULL,
normalized_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, variant_id, version_no)
);
CREATE TABLE IF NOT EXISTS variant_prices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
variant_id UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
location_id UUID,
channel VARCHAR(40) NOT NULL DEFAULT 'default',
currency CHAR(3) NOT NULL DEFAULT 'INR',
mrp NUMERIC(12,2),
base_price NUMERIC(12,2) NOT NULL,
tax_rate NUMERIC(6,2) NOT NULL DEFAULT 0,
effective_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
effective_to TIMESTAMPTZ,
source_system_id UUID REFERENCES source_systems(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_variant_prices_lookup
ON variant_prices (tenant_id, variant_id, location_id, channel, effective_from DESC);
Notes:
- keep
price_listsonly as an optional business construct - make
variant_pricesthe operational price truth - source mappings are mandatory if POS, ERP, seller, and dark-store feeds all need to resolve to one canonical variant
2. Inventory Core in catalog-service¶
Use location-aware inventory here as the canonical truth, even if it later
becomes a dedicated inventory-service.
CREATE TABLE IF NOT EXISTS stock_positions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
variant_id UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
location_id UUID NOT NULL,
quantity_on_hand NUMERIC(18,4) NOT NULL DEFAULT 0,
quantity_reserved NUMERIC(18,4) NOT NULL DEFAULT 0,
quantity_available NUMERIC(18,4) GENERATED ALWAYS AS
(quantity_on_hand - quantity_reserved) STORED,
reorder_point NUMERIC(18,4) NOT NULL DEFAULT 0,
max_stock NUMERIC(18,4),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, variant_id, location_id)
);
CREATE TABLE IF NOT EXISTS stock_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
variant_id UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
location_id UUID NOT NULL,
movement_type VARCHAR(40) NOT NULL,
quantity_delta NUMERIC(18,4) NOT NULL,
reference_type VARCHAR(40),
reference_id VARCHAR(120),
source_system_id UUID REFERENCES source_systems(id) ON DELETE SET NULL,
occurred_at TIMESTAMPTZ NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS stock_reservations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
variant_id UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
location_id UUID NOT NULL,
order_id VARCHAR(120) NOT NULL,
reserved_qty NUMERIC(18,4) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, location_id, variant_id, order_id)
);
CREATE TABLE IF NOT EXISTS orderability_projection (
tenant_id UUID NOT NULL,
variant_id UUID NOT NULL REFERENCES variants(id) ON DELETE CASCADE,
location_id UUID NOT NULL,
is_orderable BOOLEAN NOT NULL,
reason_code VARCHAR(40),
available_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, variant_id, location_id)
);
Notes:
stock_movementsbecomes the durable ledgerstock_positionsis the current aggregatestock_reservationsprevents oversellingorderability_projectionis what the UI should read for fast in-stock decisions
3. Store and Dark Store Operations in store-hub¶
Keep the existing operational tables:
storeslocationsdark_storesdark_store_zonesdark_store_pickersdark_store_ordersdark_store_order_itemsdark_store_metrics
Replace stock truth in store-hub with a projection-oriented table:
CREATE TABLE IF NOT EXISTS dark_store_catalog_projection (
tenant_id UUID NOT NULL,
dark_store_id UUID NOT NULL REFERENCES dark_stores(id) ON DELETE CASCADE,
variant_id UUID NOT NULL,
sku VARCHAR(100) NOT NULL,
product_name VARCHAR(300) NOT NULL,
category_name VARCHAR(200),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_essential BOOLEAN NOT NULL DEFAULT FALSE,
demand_score NUMERIC(8,4) NOT NULL DEFAULT 0,
velocity_rank INTEGER,
quantity_available NUMERIC(18,4) NOT NULL DEFAULT 0,
effective_price NUMERIC(12,2) NOT NULL DEFAULT 0,
orderable BOOLEAN NOT NULL DEFAULT TRUE,
source_event_id UUID,
refreshed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, dark_store_id, variant_id)
);
CREATE INDEX IF NOT EXISTS idx_ds_catalog_projection_sku
ON dark_store_catalog_projection (tenant_id, dark_store_id, sku);
Notes:
- this table is a read model only
- it replaces
dark_store_skusas the table the UI reads quantity_availableandeffective_priceare copied from canonical events, not edited directly instore-hub
4. Optional Restaurant Menu Extension in catalog-service¶
If food delivery is a near-term target, add the following instead of creating a separate repo immediately:
CREATE TABLE IF NOT EXISTS restaurant_menus (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
restaurant_id UUID NOT NULL,
menu_version INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
source_system_id UUID REFERENCES source_systems(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, restaurant_id, menu_version)
);
CREATE TABLE IF NOT EXISTS menu_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
menu_id UUID NOT NULL REFERENCES restaurant_menus(id) ON DELETE CASCADE,
external_item_id VARCHAR(200),
name VARCHAR(300) NOT NULL,
description TEXT,
price NUMERIC(12,2) NOT NULL,
tax_rate NUMERIC(6,2) NOT NULL DEFAULT 0,
veg_flag BOOLEAN,
available BOOLEAN NOT NULL DEFAULT TRUE,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Event Contracts¶
Standard Envelope¶
All domain events should use one envelope shape across catalog-service,
store-hub, sync-service, and retail-agent.
{
"event_id": "1ef9b44b-2d9f-4a2c-a8b4-1f4e6b3f9ad6",
"event_type": "inventory.stock.adjusted",
"source": "catalog-service",
"tenant_id": "9c6a3381-7c14-4ed4-9ca9-4fd0e64f0f28",
"occurred_at": "2026-03-25T10:45:12.000Z",
"trace_id": "req_01HT...",
"schema_version": 1,
"data": {}
}
Events catalog-service should publish¶
catalog.product.upsertedcatalog.variant.upsertedcatalog.variant.deletedcatalog.price.updatedcatalog.assortment.updatedcatalog.ingestion.completedinventory.stock.adjustedinventory.stock.reservedinventory.stock.releasedinventory.stock.committedinventory.orderability.changed
Example:
{
"event_type": "catalog.variant.upserted",
"source": "catalog-service",
"tenant_id": "tenant-uuid",
"schema_version": 1,
"data": {
"variant_id": "variant-uuid",
"product_id": "product-uuid",
"sku": "AMUL-BUTTER-500G",
"name": "Amul Butter 500g",
"barcode": "8901262150101",
"category_id": "category-uuid",
"attributes": {
"pack_size": "500 g"
},
"source_system_id": "source-uuid",
"version_no": 12
}
}
{
"event_type": "inventory.stock.adjusted",
"source": "catalog-service",
"tenant_id": "tenant-uuid",
"schema_version": 1,
"data": {
"movement_id": "movement-uuid",
"variant_id": "variant-uuid",
"location_id": "location-uuid",
"movement_type": "sale_committed",
"quantity_delta": -2,
"quantity_on_hand": 14,
"quantity_reserved": 0,
"quantity_available": 14,
"reference_type": "order",
"reference_id": "ORD-10024"
}
}
Events store-hub should publish¶
store.location.createdstore.location.updatedstore.dark_store.createdstore.dark_store.status.changedstore.dark_store.capacity.changedstore.dark_store.order.createdstore.dark_store.order.status.changed
Example:
{
"event_type": "store.dark_store.status.changed",
"source": "store-hub",
"tenant_id": "tenant-uuid",
"schema_version": 1,
"data": {
"dark_store_id": "dark-store-uuid",
"store_id": "store-uuid",
"status": "maintenance",
"is_active": false,
"reason": "inventory_audit"
}
}
Events retail-agent should publish¶
catalog.enrichment.completedcatalog.enrichment.failedincident.createdincident.resolvedrelease.deployed
These should enrich the commerce domain but never become the commerce domain source of truth.
Gateway Changes¶
Keep direct proxying for most routes. Add only a few composed routes in
api-gateway.
New BFF reads¶
GET /api/v1/stores/:storeId/catalog-viewGET /api/v1/dark-stores/:darkStoreId/catalog-viewGET /api/v1/stores/:storeId/searchGET /api/v1/restaurants/:restaurantId/menu
BFF behavior¶
/catalog-view should compose:
- store or dark-store status from
store-hub - canonical catalog from
catalog-service - orderability from
catalog-service - effective pricing from
catalog-service - optional assortment projection from
store-hub
The gateway should not perform synchronous fan-out per item. It should call one upstream endpoint that already returns a store-scoped view, or call two coarse read endpoints and merge once.
Sync-Service Changes¶
Align sync-service with the actual current service graph.
Required changes¶
- replace
product-serviceendpoint assumptions withcatalog-service - replace
inventory-serviceendpoint assumptions withcatalog-service - keep customer-facing sync flows pointed at
store-hubor the eventual owner - continue consuming
catalog.events, but upgrade handlers for new event names
Updated mapping¶
product->catalog-serviceinventory_adjustment->catalog-servicecustomer->store-hubsale-> store order or transaction servicepayment-> billing or payment service
Repo-by-Repo Implementation Plan¶
Slice 1: Make inventory ownership explicit¶
In catalog-service:
- add
stock_positions - add
stock_movements - add
stock_reservations - publish
inventory.stock.adjusted - publish
inventory.orderability.changed
In store-hub:
- stop writing stock truth to
inventory_by_location - stop treating
dark_store_skus.quantity_on_handas authoritative - start consuming inventory events into a projection table
Slice 2: Normalize prices and source mappings¶
In catalog-service:
- add
source_systems - add
source_entities - add
catalog_item_versions - add
variant_prices - move ingestion pipeline persistence to canonical IDs instead of loose SKU-only writes
Slice 3: Introduce composed read models¶
In catalog-service:
- expose
GET /internal/store-catalog-view?location_id=...
In store-hub:
- expose
GET /internal/dark-store-status/:id
In api-gateway:
- add BFF route for
catalog-view
Slice 4: Restaurant menu vertical¶
In catalog-service:
- add menu tables
- add POS menu adapters
- publish
menu.item.upserted - publish
restaurant.status.changed
Recommended Final Boundary¶
If you later split inventory into its own repo, the target boundary should be:
catalog-service: products, variants, pricing, ingestion, source mappingsinventory-service: stock, reservations, orderabilitystore-hub: stores, dark stores, fulfillment workflowsapi-gateway: edge and thin BFFsync-service: offline replicationretail-agent: enrichment and control plane
Until then, the lowest-risk path is:
- keep catalog and inventory together in
catalog-service - turn
store-hubstock tables into projections - standardize events
- add one store-scoped catalog read path