Skip to content
All projects

Flagship system

CommerceOS

Multi-Tenant Commerce · Architecture

A multi-tenant, headless commerce platform whose backend is deliberately vertical-neutral — the same engine powers a fashion boutique, a grocery, a pharmacy or a B2B wholesaler, with each store's identity expressed as data rather than code.

11
Technologies
2
Domains
6
Capabilities
—
Public links

Who it's for

Businesses of any vertical — fashion, grocery, pharmacy, B2B wholesale — that need a real commerce backend without building one from scratch.

Where it fits

As the headless engine behind one store or a whole portfolio of tenants: one deployment, many isolated storefronts, any frontend.

The hard part

The problem

Commerce backends usually hardcode the business type — a grocery engine, a fashion engine, a pharmacy engine. Adding a new vertical means forking the code and threading business-type conditionals through the domain, which rots fast.

Key constraints

The core had to stay strictly vertical-neutral: never a single business-type conditional branch anywhere in the backend. Independent stores had to run on one deployment without ever seeing each other's data, and prices had to resolve correctly per market — not just per currency.

Architecture overview

Clean Architecture with CI-enforced dependency boundaries. The domain models the commerce network (tenant → catalogue → category → product → variant) with dynamic attributes and pluggable product-type strategies. Every external integration — payment, email, storage, search — sits behind an abstract service with swappable adapters resolved per tenant, so the core depends only on interfaces.

One engine · isolated tenants

Vertical-neutral core
Tenant · fashion
Tenant · grocery
Tenant · pharmacy
Tenant · B2B

every tenant isolated at the query layer — one engine, zero data bleed

One deployment hosts many tenants. Each tenant-scoped row carries a tenant identifier and isolation is enforced at the query layer, so independent stores share one engine and one catalogue model without ever reading each other's data.

How it works

What's actually in it.

  1. 01Multi-tenant isolation enforced at the query layer — every tenant-scoped row carries a tenant identifier.
  2. 02Catalogue engine end to end: products, a variant engine with an enforced generation ceiling, dynamic attributes, brands, collections, tags and SEO / JSON-LD / sitemap generation.
  3. 03Global-market pricing engine with a batched, N+1-safe read path folding price into product detail and variants.
  4. 04Ports-and-adapters for Stripe/PayPal, SMTP/SES, Local/S3, Postgres-FTS/OpenSearch, resolved per tenant.
  5. 05API-first: standard response envelope, OpenAPI schema and versioned REST contract so the UI can be replaced with zero backend change.
  6. 06Clean Architecture with import-linter-enforced boundaries, strict mypy with zero suppressions, UUIDv7 keys and feature flags.

Engineering decisions

Market modelled as distinct from currency

Naïvely, price is 'amount + currency'. But the same currency can span markets with different pricing, and a market can be served without a locked local price. CommerceOS models market as a first-class concept with explicit locked prices, an FX reference-currency fallback and a deterministic resolver that never borrows a price between markets.

The trade-off

Modelling market separately from currency adds concepts and a resolver most shops never need. For a single-country store it is overkill — but it is the difference between a platform that expands globally and one that has to be re-architected the first time it does.

The obvious approach

Build the store for its first vertical and add a `business_type` flag when a second one appears.

Why I didn't choose it

Every flag multiplies conditional branches across the domain; the code becomes a maze of 'if pharmacy… else grocery…' that no one can safely change.

My approach

Express identity as data and configuration — dynamic attributes and pluggable product-type strategies — so a new vertical is a configuration change, never a code fork.

Trade-offs

More upfront abstraction and discipline (import-linter, strict mypy) than a hardcoded store, repaid the first time a genuinely different vertical ships without touching the core.

Technical highlights

  • A new vertical is a configuration change, not a code fork.
  • Per-tenant adapter resolution keeps the core dependent only on interfaces.
  • Governance via a constitution, ADRs and per-phase validation reports.

Claims & evidence

  • No business-type conditional branch exists anywhere in the backend — vertical identity is data, not code.

    Qualitative · CommerceOS design constraint

Every statement above carries its evidence class — nothing is invented.

Tech stack

  • Python 3.12
  • Django 5.2
  • Django REST Framework
  • Django Channels
  • Celery
  • Redis
  • PostgreSQL
  • Next.js 15
  • TypeScript
  • TanStack Query
  • Docker

Want to go deeper on CommerceOS?

I'm happy to walk through the architecture and the decisions behind it.

Get in touch