$ open blueprint
Ask most warehouse managers:
"How much of SKU-4471 do we actually have?"
The honest answer is often a pause.
Then:
"Let me check three systems and call you back."
The ERP says 2,400 units.
The warehouse management system says 2,317.
A cycle count from yesterday says 2,281.
Sales has already reserved another 340 units.
And 500 more units are sitting on a truck between Warehouse A and Warehouse B.
None of those numbers are necessarily wrong.
They are simply answering different questions.
The problem is architectural.
Inventory is frequently modeled as a mutable number instead of a traceable history of physical events.
Nexus SCM takes the opposite approach.
Inventory is modeled as a ledger of movements.
Receiving creates inventory.
Picking consumes inventory.
Transfers move inventory between locations.
Cycle counts create explicit adjustments.
Reservations create commitments.
Shipments change ownership and availability.
Every important state transition produces a durable domain event.
The result is a system where:
- "How much physically exists?"
- "Where is it?"
- "How much is available?"
- "How much is reserved?"
- "How much is in transit?"
- "Why did the quantity change?"
- "What did we have last Tuesday?"
are all answerable from the same underlying operational truth.
Executive Summary
Nexus SCM is designed as a ledger-first, event-driven, multi-tenant supply chain platform.
The architecture separates operational writes from analytical reads while maintaining PostgreSQL as the transactional source of truth for inventory and business state.
The core architectural principle is:
Never make the current inventory number the source of truth. Make the history of inventory movements the source of truth.
Current inventory becomes a projection of that history.
The platform is organized around bounded business domains:
| Domain | Primary Responsibility |
|---|---|
| Identity & Access | Authentication, authorization, tenant isolation, warehouse permissions |
| Organization | Tenant configuration, branches, warehouses, operational policies |
| Product Catalog | SKU, product, unit of measure, barcode and packaging definitions |
| Procurement | Requisitions, RFQs, supplier quotations, purchase orders |
| Receiving | ASN, receiving sessions, inspections, discrepancy handling |
| Warehouse Operations | Putaway, picking, packing, cycle counting, bin operations |
| Inventory Ledger | Immutable stock movements and inventory state transitions |
| Reservation | Stock commitments against sales and fulfillment demand |
| Transfer | Warehouse-to-warehouse and location-to-location movement |
| Distribution | Shipment planning, allocation, carrier integration |
| Supplier Collaboration | Supplier confirmations, ASN, supplier performance |
| Forecasting | Demand velocity, safety stock, lead-time analysis |
| Replenishment | Reorder points, purchase requisitions, replenishment policies |
| Reporting | CQRS projections and analytical read models |
| Audit | Immutable business and security audit trail |
The architecture intentionally separates business correctness from eventual read optimization.
PostgreSQL owns transactional correctness.
Kafka distributes committed events.
Read models serve high-volume queries.
Celery handles asynchronous jobs.
Redis accelerates frequently accessed projections and operational lookups.
Business Problem
Consider a distributor operating:
- 8 warehouses
- 35,000 SKUs
- 150 suppliers
- 4,000 inbound receipts per month
- 50,000 outbound order lines per day
- thousands of barcode scans per hour
- multiple transportation providers
A naïve inventory system might store:
sku.total_stock = 2317
That number looks useful.
It is actually insufficient.
It cannot answer:
- Why is stock 2,317?
- Who changed it?
- Which warehouse owns it?
- How much is reserved?
- How much is damaged?
- How much is available for sale?
- How much is currently in transit?
- What was the quantity yesterday?
- Which receiving operation created the stock?
- Which picks consumed it?
- Was the quantity adjusted after a cycle count?
Nexus models these questions explicitly.
Architectural Principles
The platform follows several non-negotiable architectural principles.
1. Ledger First
Inventory is derived from immutable stock movements.
2. Tenant First
Every business record belongs to an organization.
Cross-tenant access is never inferred from user identity alone.
3. Location Scoped
Inventory operations are scoped to warehouses, zones, bins, and physical locations.
4. Transactional Truth Before Events
The database transaction commits the business truth first.
Events are published only after the transaction commits.
5. Idempotent Consumers
Every external and internal event consumer must safely process duplicates.
6. Explicit State Machines
Important workflows are represented as valid state transitions instead of arbitrary status updates.
7. CQRS for Read Scalability
Operational writes and analytical reads have different models.
8. No Distributed Transaction Across Services
The system uses transactional boundaries, outbox events, idempotency, and eventual consistency rather than trying to maintain a global distributed transaction.
9. Auditability by Default
Every consequential inventory and procurement action has a traceable actor, timestamp, reference, and reason.
10. Physical Reality Is the Source of Business Meaning
The system should model what physically happened, not merely what someone clicked in the UI.
High-Level Architecture
┌──────────────────────────┐
│ Client Applications │
│ │
│ Web · Scanner · Mobile │
│ Supplier Portal · APIs │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ API Gateway │
│ │
│ Auth · Rate Limit · TLS │
│ Routing · Tenant Context │
└────────────┬─────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Procurement │ │ Warehouse │ │ Distribution │
│ Domain │ │ Domain │ │ Domain │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────────────┼─────────────────────────┘
│
▼
┌──────────────────────────┐
│ Inventory Domain │
│ │
│ Ledger · Reservation │
│ Transfer · Availability │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ PostgreSQL │
│ │
│ Transactional State │
│ Inventory Ledger │
│ Outbox │
└────────────┬─────────────┘
│
│ committed events
▼
┌──────────────────────────┐
│ Transactional │
│ Outbox │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Kafka │
│ │
│ Domain Event Backbone │
└────────────┬─────────────┘
│
┌───────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ Read Model │ │ Forecasting │ │ Notifications │
│ Consumers │ │ & Reorder │ │ & Integrations │
└──────┬──────┘ └─────────────┘ └────────────────┘
│
▼
┌────────────────┐
│ Query Stores │
│ / Projections │
└────────────────┘
┌───────────────────────┐
│ Celery │
│ │
│ Forecasting │
│ Reconciliation │
│ Scheduled Jobs │
└───────────┬───────────┘
│
▼
Redis
Why PostgreSQL Remains the Transactional Core
Kafka is not the source of truth for inventory.
This distinction matters.
A warehouse receipt must guarantee that:
Receipt recorded
+
Inventory movement recorded
+
PO line updated
+
Outbox event recorded
either all happen or none happen.
The transaction should therefore look conceptually like:
with transaction.atomic():
receipt = create_receipt(...)
movement = StockMovement.objects.create(
warehouse=warehouse,
sku=sku,
quantity=received_quantity,
movement_type="receipt",
reference_id=receipt.id,
)
purchase_order_line.mark_received(received_quantity)
OutboxEvent.objects.create(
aggregate_type="stock_movement",
aggregate_id=movement.id,
event_type="inventory.stock_received",
payload=event_payload,
)
Only after PostgreSQL commits should the event become available to Kafka.
This prevents a dangerous situation where:
Database transaction succeeds
│
├── Inventory updated
│
└── Kafka publish fails
without any durable record of the event.
The Outbox Pattern closes that gap.
Transactional Outbox Pattern
The architecture uses PostgreSQL as the transactional boundary and an outbox table as the bridge between database state and Kafka.
Warehouse Scan
│
▼
Application Service
│
▼
PostgreSQL Transaction
│
├── StockMovement
├── Receipt
├── Business State
└── OutboxEvent
│
▼
COMMIT
│
▼
Outbox Publisher
│
▼
Kafka
If Kafka is temporarily unavailable:
PostgreSQL
│
└── OutboxEvent remains pending
│
▼
Retry Publisher
│
▼
Kafka
No inventory transaction needs to wait for Kafka availability.
This is a critical distinction between an architectural diagram that merely contains Kafka and an architecture that can actually survive production failures.
Inventory Domain
The Inventory Domain is the operational center of Nexus SCM.
Its responsibility is not simply:
"How many units do we have?"
It answers:
What happened to every unit,
where is it now,
what state is it in,
and who or what caused the change?
The domain contains several concepts:
SKU
│
├── Inventory Balance
│
├── Stock Movement
│
├── Reservation
│
├── Inventory State
│
├── Location
│
└── Availability
Inventory Ledger
A simplified ledger model:
class StockMovement:
organization_id
warehouse_id
location_id
sku_id
movement_type
quantity
reference_type
reference_id
occurred_at
recorded_at
actor_id
idempotency_key
metadata
Examples:
+500 receipt
-20 pick
-5 damage
+5 cycle-count-adjustment
-50 transfer-out
+50 transfer-in
The ledger is append-only.
A correction does not modify the previous record.
Instead:
Incorrect Movement
│
▼
Compensating Movement
For example:
Original:
+100 received
Correction:
-10 receiving_adjustment
The original event remains visible.
This gives the platform forensic capability.
Inventory Equation
For a given SKU and warehouse:
On Hand
=
Σ all confirmed inventory movements
But operational availability is more nuanced:
Available
=
On Hand
- Reserved
- Allocated
- Blocked
And network availability may include:
Available to Promise
=
Available Now
+ Eligible Incoming Stock
- Existing Commitments
This distinction prevents the common mistake of treating:
physical stock
and:
sellable stock
as the same number.
Inventory States
Nexus should explicitly model inventory state.
A useful model is:
AVAILABLE
RESERVED
ALLOCATED
PICKED
PACKED
DAMAGED
QUARANTINED
BLOCKED
IN_TRANSIT
The important point is that these states should not simply be arbitrary strings.
They represent business invariants.
For example:
QUARANTINED stock
↓
cannot become AVAILABLE
↓
until an authorized disposition occurs
Similarly:
RESERVED
↓
ALLOCATED
↓
PICKED
↓
PACKED
↓
SHIPPED
The system should reject invalid transitions.
Warehouse Hierarchy
Inventory needs more granularity than warehouse-level ownership.
A realistic hierarchy is:
Organization
│
└── Warehouse
│
├── Zone
│ │
│ ├── Aisle
│ │ │
│ │ └── Bin
│ │
│ └── Aisle
│
├── Receiving Area
├── Picking Area
├── Packing Area
├── Quarantine Area
└── Dispatch Area
Therefore:
Stock
=
Organization
+
Warehouse
+
Location
+
SKU
+
Inventory State
This allows queries such as:
How much SKU-4471 is available
in Warehouse A
inside Zone C
excluding quarantine stock?
without introducing duplicate inventory records.
Inventory Reservation
Reservations should not mutate physical inventory.
A reservation represents a commitment.
Physical Inventory
│
├── 1,000 On Hand
│
└── 300 Reserved
│
▼
700 Available
This distinction is essential.
A sales order may reserve inventory before warehouse staff physically pick it.
Therefore:
Reservation ≠ Stock Movement
The two concepts are related but not identical.
Reservation Lifecycle
Demand Created
│
▼
Reservation Requested
│
▼
Availability Checked
│
▼
Reservation Created
│
├───────────────┐
▼ ▼
Allocated Released
│
▼
Picked
│
▼
Consumed
If an order is cancelled:
Reserved
│
▼
Released
No fake inventory movement is necessary simply because a reservation disappeared.
Warehouse Transfers
Warehouse transfers are modeled as a stateful workflow.
TRANSFER_REQUESTED
│
▼
ALLOCATED
│
▼
DISPATCHED
│
▼
IN_TRANSIT
│
▼
RECEIVED
│
▼
COMPLETED
The physical inventory equation must remain correct at every stage.
Example:
Warehouse A
-50
In Transit
+50
Warehouse B
0
Total network inventory:
50
No inventory has been created or destroyed.
When Warehouse B receives:
Warehouse A
-50
In Transit
0
Warehouse B
+50
Total:
50
Transfer Idempotency
A receiving scanner may retry the same request because the handheld device temporarily loses connectivity.
Without idempotency:
Receive 50
Retry
Receive 50 again
would produce:
+100
instead of:
+50
Therefore every physical operation should carry an idempotency key:
warehouse_id
+
device_id
+
operation_id
Example:
scanner-17:transfer-8842-receive
The database enforces uniqueness.
Warehouse Receiving
Receiving begins before inventory becomes available.
Advanced Shipping Notice
│
▼
Inbound Shipment
│
▼
Receiving Session
│
▼
Barcode Scan
│
▼
Quantity Validation
│
▼
Quality Inspection
│
├── Accepted
│
└── Quarantined
│
▼
Inventory Movement
│
▼
Putaway Task
This avoids the dangerous assumption:
"Scanned" = "Available"
A damaged or quarantined item should not automatically become sellable stock.
Putaway Workflow
Received
│
▼
Putaway Task Created
│
▼
Optimal Bin Selected
│
▼
Picker Scans Item
│
▼
Picker Scans Destination Bin
│
▼
Location Validation
│
▼
Putaway Confirmed
The system should validate:
SKU matches
+
warehouse matches
+
destination is valid
+
quantity is valid
+
task is still active
before committing the movement.
Picking Architecture
Picking is modeled as a workflow rather than a simple stock decrement.
Order
│
▼
Allocation
│
▼
Pick Wave
│
▼
Pick Task
│
▼
Scanner
│
▼
SKU Verification
│
▼
Bin Verification
│
▼
Quantity Confirmation
│
▼
Stock Movement
│
▼
Packing
The scanner should never directly manipulate a stock quantity.
It sends a command.
The application validates the command.
The inventory domain creates the movement.
Command vs Event
This distinction is important.
A scanner sends a command:
ConfirmPick
The system produces an event:
inventory.stock_picked
Commands express intent.
Events express facts.
Command:
"Pick 5 units."
Event:
"5 units were picked."
This distinction keeps domain boundaries clean.
Event-Driven Architecture
The event bus is not responsible for determining whether an operation is valid.
The domain transaction determines validity.
Kafka distributes the resulting fact.
Example:
ConfirmReceiving Command
│
▼
Inventory Transaction
│
├── StockMovement
├── Receipt
└── OutboxEvent
│
▼
Kafka
│
┌────────────┼───────────────┐
▼ ▼ ▼
Procurement Forecasting Audit Projection
The receiving service does not directly call forecasting.
It publishes a fact.
Forecasting decides whether it cares.
Domain Event Structure
Events should contain stable metadata.
Example:
{
"event_id": "evt_01JX...",
"event_type": "inventory.stock_received",
"event_version": 1,
"occurred_at": "2026-08-18T10:30:00Z",
"organization_id": "org_123",
"warehouse_id": "wh_001",
"aggregate_type": "stock_movement",
"aggregate_id": "mov_88321",
"correlation_id": "rcv_8821",
"causation_id": "cmd_9912",
"payload": {
"sku_id": "SKU-4471",
"quantity": 500,
"location_id": "BIN-A-17"
}
}
Important metadata includes:
event_idevent_typeevent_versionorganization_idaggregate_idcorrelation_idcausation_idoccurred_at
This makes distributed debugging significantly easier.
Kafka Topic Strategy
Events should be organized by business domain rather than creating one giant topic.
Example:
inventory.events
warehouse.events
procurement.events
transfer.events
shipment.events
supplier.events
Partitioning should preserve ordering where ordering matters.
For inventory events, a useful partition key may be:
organization_id + warehouse_id + sku_id
This helps preserve event ordering for a specific inventory stream while allowing unrelated SKUs to process in parallel.
Event Ordering
Distributed systems cannot assume global event ordering.
Consider:
Pick Confirmed
Shipment Confirmed
Carrier Event
These may arrive at consumers in an unexpected order.
Consumers should therefore use:
event timestamp
+
sequence/version
+
state transition validation
rather than assuming:
event A always arrives before event B
Idempotent Event Consumption
Kafka provides at-least-once delivery semantics in many practical architectures.
Therefore consumers must assume:
same event
↓
may arrive twice
A consumer can maintain:
ProcessedEvent
----------------
event_id
consumer_name
processed_at
Before processing:
if already_processed(event.event_id):
return
The important part is that event processing and recording the processed state should itself be atomic where possible.
Failure Handling
A production architecture must assume failure.
Examples:
Kafka unavailable
Database unavailable
Scanner retries
Supplier sends duplicate ASN
Carrier sends duplicate webhook
Consumer crashes after processing
Consumer crashes before acknowledging
Network timeout after successful transaction
Nexus handles these through:
Transactions
+
Outbox
+
Idempotency
+
Retries
+
Dead Letter Queues
+
Reconciliation Jobs
Dead Letter Queue
Events that repeatedly fail should not block an entire consumer.
Kafka
│
▼
Consumer
│
├── Success ───────────────▶ Processed
│
└── Failure
│
▼
Retry
│
├── Success
│
└── Repeated Failure
│
▼
DLQ
The DLQ should preserve enough information to investigate and replay the event safely.
Reconciliation
Even a strongly designed distributed system needs reconciliation.
Examples:
Inventory Projection
vs
Ledger
PO Received Quantity
vs
Inventory Receipts
Shipment State
vs
Carrier State
Scheduled reconciliation jobs can detect divergence.
Ledger
│
▼
Recalculate Projection
│
▼
Compare
│
├── Match
│
└── Divergence
│
▼
Alert / Repair
This is particularly important for long-running enterprise systems.
CQRS Architecture
The operational database should not become the analytical warehouse.
The write model is optimized for:
transactions
consistency
constraints
append operations
state transitions
The read model is optimized for:
aggregation
filtering
dashboards
historical reporting
warehouse comparison
forecasting
Architecture:
┌─────────────────────┐
│ PostgreSQL Write DB │
│ │
│ Source of Truth │
└──────────┬──────────┘
│
▼
Domain Events
│
▼
Kafka
│
┌──────────────────┼─────────────────┐
▼ ▼ ▼
Inventory Read Procurement Read Analytics Read
Model Model Model
│ │ │
└──────────────────┴─────────────────┘
│
▼
Reporting APIs
Read Model Design
Instead of executing:
SELECT
warehouse,
sku,
SUM(quantity)
FROM stock_movements
GROUP BY warehouse, sku;
for every dashboard request, Nexus can maintain:
inventory_balance_projection
containing:
organization_id
warehouse_id
location_id
sku_id
on_hand
reserved
allocated
available
in_transit
last_movement_at
This allows the operational UI to answer:
"How much SKU-4471 is available?"
without scanning millions of historical movements.
Rebuildable Projections
Read models are disposable.
If the projection logic changes:
Ledger
│
▼
Replay Events
│
▼
New Projection
The system does not need to modify historical truth.
This gives the architecture a powerful property:
The read model can evolve independently from the source of truth.
Procurement Architecture
Procurement is modeled as a state machine.
REQUISITION
│
▼
RFQ
│
▼
QUOTATION
│
▼
APPROVAL
│
▼
PURCHASE ORDER
│
▼
SUPPLIER CONFIRMED
│
▼
SHIPPED
│
▼
RECEIVED
│
▼
CLOSED
Invalid transitions should be rejected.
For example:
CLOSED → SUPPLIER_CONFIRMED
should not be possible without an explicit reversal workflow.
Three-Way Matching
Procurement and finance should converge around:
Purchase Order
+
Goods Receipt
+
Supplier Invoice
The system compares:
Ordered Quantity
Received Quantity
Invoiced Quantity
and:
Ordered Price
Received Price
Invoiced Price
A mismatch becomes an exception rather than silently progressing toward payment.
Supplier Integration
External suppliers are inherently unreliable.
They may:
- send duplicate confirmations
- resend ASNs
- change delivery dates
- send partial shipments
- send incorrect quantities
- fail to respond
Therefore supplier integrations should be designed as asynchronous boundaries.
Supplier
│
▼
Integration Gateway
│
▼
Validation
│
▼
Idempotency
│
▼
Domain Command
│
▼
Procurement Domain
External payloads should never directly mutate internal database state.
Distribution Architecture
Outbound fulfillment follows:
Customer Demand
│
▼
Inventory Availability
│
▼
Reservation
│
▼
Allocation
│
▼
Pick Wave
│
▼
Picking
│
▼
Packing
│
▼
Shipment
│
▼
Carrier
│
▼
Delivery
Each stage produces an explicit business fact.
Carrier Integration
Carrier events arrive asynchronously.
Example:
shipment.created
shipment.dispatched
shipment.in_transit
shipment.out_for_delivery
shipment.delivered
shipment.exception
Carrier event IDs should be used for deduplication.
The system should not assume carrier events arrive exactly once or in order.
Multi-Tenant Architecture
The tenant boundary is fundamental.
Organization
│
├── Users
├── Warehouses
├── Suppliers
├── Products
├── Orders
├── Inventory
└── Procurement
Every tenant-owned record should carry:
organization_id
where appropriate.
Application services should establish tenant context before executing domain queries.
Conceptually:
queryset.filter(
organization_id=current_organization.id
)
But tenant isolation should not rely solely on developers remembering filters.
Defense in depth may include:
Application-level tenant isolation
+
Database constraints
+
PostgreSQL Row-Level Security where appropriate
+
Authorization policies
+
Automated tests
Warehouse-Scoped Authorization
Tenant membership alone is insufficient.
A user may belong to an organization but only have access to:
Warehouse A
The authorization model can therefore represent:
User
│
└── Organization Membership
│
├── Role
│
└── Warehouse Scope
Example:
Warehouse Picker
→ Warehouse A
→ Read inventory
→ Execute picks
→ Cannot adjust inventory
Warehouse Manager
→ Warehouse A
→ Warehouse B
→ Adjust inventory
→ Approve cycle counts
Procurement Manager
→ All warehouses
→ Suppliers
→ Purchase Orders
Permissions should follow least privilege.
Product and SKU Architecture
A product is not necessarily the same thing as a stock-keeping unit.
A realistic model separates:
Product
│
└── SKU
│
├── Barcode
├── Unit of Measure
├── Packaging
├── Supplier SKU
└── Warehouse Configuration
This allows:
1 carton = 24 units
without corrupting the underlying inventory model.
Units of Measure
Supply chains frequently operate across units.
Examples:
Piece
Box
Carton
Pallet
Kilogram
Liter
Meter
Conversions should be explicit.
1 carton
=
24 pieces
The system should define which unit represents the inventory base quantity.
For example:
Base Unit = piece
while purchasing may occur in:
carton
This avoids ambiguous quantities.
Cycle Counting
Cycle counts should not overwrite inventory.
Instead:
Expected Quantity
│
▼
Physical Count
│
▼
Variance
│
▼
Approval
│
▼
Adjustment Movement
Example:
Expected = 200
Physical = 178
Variance = -22
The system creates:
cycle_count_adjustment = -22
The original expected balance remains reconstructable.
Forecasting Architecture
Forecasting should consume movement history rather than manually maintained spreadsheets.
Stock Movements
│
▼
Consumption Projection
│
▼
Demand Velocity
│
▼
Seasonality / Trend
│
▼
Forecast
│
▼
Replenishment Engine
Forecasting can consider:
Historical demand
Lead time
Safety stock
Seasonality
Supplier reliability
Minimum order quantity
Order frequency
Current inventory
Incoming inventory
Existing reservations
Reorder Point
A simplified model:
Reorder Point
=
Demand During Lead Time
+
Safety Stock
If:
Current Available
<
Reorder Point
the system can generate a replenishment recommendation.
But auto-purchasing should be governed by policy.
Forecast
│
▼
Reorder Recommendation
│
▼
Policy Engine
│
├── Auto Approve
│
├── Manager Approval
│
└── Manual Review
This is safer than blindly generating purchase orders.
Celery Architecture
Celery should handle work that does not belong in the synchronous request path.
Examples:
Forecast generation
Reorder calculations
Supplier synchronization
Carrier synchronization
Report generation
Projection repair
Reconciliation
Notification delivery
Data exports
The API should not block waiting for these operations.
Redis Usage
Redis should be treated as an acceleration layer, not the source of inventory truth.
Good uses:
Hot inventory projections
Rate limiting
Distributed locks where justified
Short-lived workflow state
Task coordination
Cache
Bad use:
Redis = inventory source of truth
If Redis disappears, the system should remain correct.
It may become slower.
It must not become wrong.
Concurrency Control
Inventory operations are inherently concurrent.
Two scanners might attempt:
Pick 10 units
at almost exactly the same time.
If only 12 units are available:
Request A → 10
Request B → 10
the system must not end with:
-8 available
The inventory transaction must enforce the invariant.
Possible strategies include:
Row-level locking
Optimistic concurrency
Serializable transactions where appropriate
Reservation constraints
Atomic conditional updates
The choice should be based on the exact contention characteristics of the inventory model.
Inventory Invariants
Production systems should make business invariants explicit.
Examples:
A stock movement must belong to one organization.
A warehouse must belong to the same organization as its stock movement.
A transfer-out cannot exceed transferable stock.
A quarantined quantity cannot be allocated.
A completed shipment cannot return to PACKED without an explicit reversal.
A closed purchase order cannot receive additional goods without reopening or exception handling.
A reservation cannot exceed allocatable inventory.
A duplicate physical operation must not create duplicate stock.
These invariants belong in domain services and database constraints where practical.
Database Architecture
Primary transactional store:
PostgreSQL
Logical separation may look like:
Core Transactional Tables
│
├── Organization
├── Warehouse
├── SKU
├── Inventory
├── StockMovement
├── Reservation
├── Transfer
├── PurchaseOrder
└── Shipment
Infrastructure Tables
│
├── OutboxEvent
├── IdempotencyKey
├── ProcessedEvent
└── AuditLog
Indexes should reflect operational access patterns rather than being added indiscriminately.
Inventory Indexing
Typical access patterns include:
organization + warehouse + sku
organization + sku + movement_time
warehouse + location + sku
reference_type + reference_id
idempotency_key
For very large ledgers, time-based partitioning may eventually become appropriate:
stock_movements_2026_01
stock_movements_2026_02
stock_movements_2026_03
...
Partitioning should be introduced based on measured data volume and query behavior, not simply because the architecture diagram contains millions of rows.
Scaling Strategy
Nexus should scale in layers.
Stage 1
Django
+
PostgreSQL
+
Redis
+
Celery
Stage 2
Read replicas
+
CQRS projections
+
Background workers
Stage 3
Kafka
+
Event-driven integrations
+
Independent consumers
Stage 4
Partitioned ledgers
+
Dedicated reporting infrastructure
+
Horizontal worker scaling
Stage 5
Domain extraction
+
Independent service deployment
The architecture should not start with dozens of microservices merely because the system is "enterprise."
Modular Monolith First
A realistic implementation can begin as a modular Django application:
nexus/
├── identity/
├── organizations/
├── catalog/
├── procurement/
├── receiving/
├── warehouse/
├── inventory/
├── reservations/
├── transfers/
├── distribution/
├── suppliers/
├── forecasting/
├── replenishment/
├── reporting/
├── integrations/
└── audit/
Each module owns its domain logic.
Communication should happen through:
Application Services
+
Domain Events
+
Explicit Contracts
rather than arbitrary cross-module database manipulation.
This allows the architecture to evolve toward independently deployed services later without prematurely paying the operational cost of microservices.
Service Extraction Strategy
If scale eventually requires service extraction:
Modular Monolith
│
▼
Identify High-Load Boundary
│
▼
Define Contract
│
▼
Introduce Events
│
▼
Extract Consumer
│
▼
Separate Deployment
Likely candidates for extraction could eventually include:
Forecasting
Carrier Integration
Supplier Integration
Reporting
Notification
The inventory ledger should be extracted only when there is a compelling operational reason.
It is the most consistency-sensitive domain.
API Architecture
The API should expose business operations rather than arbitrary CRUD mutations.
Prefer:
POST /receipts/{id}/confirm
POST /pick-tasks/{id}/confirm
POST /transfers/{id}/dispatch
POST /transfers/{id}/receive
POST /reservations
POST /cycle-counts/{id}/approve
over:
PATCH /inventory/123
{
"quantity": 178
}
The former expresses business intent.
The latter bypasses the domain model.
API Idempotency
Critical commands should support:
Idempotency-Key
Especially:
Receive
Pick
Ship
Transfer
Reserve
Release
Purchase Order Confirmation
A retry should return the result of the original operation instead of creating another operation.
Observability Architecture
Production supply chain systems need more than logs.
Nexus should provide:
Metrics
+
Structured Logs
+
Distributed Tracing
+
Audit Events
+
Business Monitoring
Important metrics include:
Stock movement throughput
Kafka consumer lag
Outbox backlog
Failed event count
DLQ depth
Pick completion rate
Receiving throughput
Inventory adjustment rate
Reservation failure rate
Supplier confirmation latency
Carrier event latency
Forecast execution time
Correlation and Traceability
A single warehouse operation should be traceable across the system.
Example:
Scanner Request
│
│ correlation_id
▼
API
│
▼
Inventory Transaction
│
▼
Outbox
│
▼
Kafka
│
├── Forecasting
├── Reporting
└── Audit
If a warehouse manager asks:
"Why did SKU-4471 change by 22 units?"
the system should be able to trace:
Movement
↓
Command
↓
User
↓
Device
↓
Warehouse
↓
Reference Document
↓
Related Events
Audit Architecture
Audit logging is separate from inventory history.
Inventory history answers:
What happened to stock?
Security audit answers:
Who performed what administrative action?
Examples:
User permission changed
Warehouse access granted
Supplier bank information modified
Inventory adjustment approved
Purchase order approval changed
Audit records should be immutable and access-controlled.
Security Architecture
Security boundaries exist at several levels:
Internet
│
▼
TLS
│
▼
API Gateway
│
▼
Authentication
│
▼
Tenant Resolution
│
▼
Authorization
│
▼
Warehouse Scope
│
▼
Domain Operation
Security should not depend on frontend restrictions.
The backend must enforce:
Tenant
+
Role
+
Warehouse Scope
+
Resource Ownership
+
Operation Permission
Data Consistency Model
Not every piece of information needs strong consistency.
Strong Consistency
Use transactional consistency for:
Inventory movement
Reservation creation
Transfer dispatch
Receiving confirmation
Purchase order state transition
Eventual Consistency
Use eventual consistency for:
Dashboards
Forecasts
Notifications
Search indexes
Analytics
Supplier performance projections
This distinction is fundamental.
Trying to make everything strongly consistent increases system complexity and reduces scalability without providing meaningful business value.
Source of Truth Matrix
| Data | Source of Truth |
|---|---|
| Inventory movements | PostgreSQL Ledger |
| Reservations | PostgreSQL Transactional Model |
| Purchase Orders | Procurement Domain |
| Warehouse Tasks | Warehouse Domain |
| Shipment State | Distribution Domain |
| Event Delivery | Kafka |
| Read Models | Derived Projections |
| Cache | Redis |
| Forecast | Forecasting Projection |
| Audit | Immutable Audit Store |
Kafka is therefore an event backbone, not the canonical inventory database.
Redis is a cache, not the canonical inventory database.
Read models are projections, not canonical inventory state.
Failure Scenarios
Database Commits, Kafka Fails
Database
│
├── Inventory committed
└── Outbox committed
│
▼
Kafka unavailable
Result:
Inventory remains correct.
Outbox retries later.
Kafka Publishes, Consumer Crashes
Kafka
│
▼
Consumer
│
├── Business processing
│
└── Crash
The event may be delivered again.
Idempotency ensures:
one business effect
even if:
multiple delivery attempts
occur.
Scanner Times Out
The scanner may not know whether the operation succeeded.
It retries using:
same idempotency key
The server responds with the original result.
Carrier Sends Duplicate Webhook
carrier_event_id = 77881
is already processed.
The second delivery becomes a no-op.
Disaster Recovery
The transactional database is the critical recovery asset.
The architecture should provide:
Automated PostgreSQL backups
+
Point-in-time recovery
+
Replica strategy
+
Kafka retention
+
Projection rebuild capability
A key architectural property is:
Read models can be recreated. Operational truth cannot be casually recreated after being destroyed.
Therefore backup and recovery priorities should focus first on transactional truth.
Disaster Recovery Flow
Primary PostgreSQL Failure
│
▼
Recovery / Replica
│
▼
Restore Transactional State
│
▼
Resume Outbox Publishing
│
▼
Kafka Consumers Resume
│
▼
Rebuild Missing Projections
Security and Operational Boundaries
The architecture should distinguish:
Business Data
Operational Events
Security Audit
Integration Credentials
Secrets
Logs
Sensitive credentials should never appear in:
Kafka payloads
application logs
audit metadata
error responses
Secrets should be managed through a dedicated secrets-management mechanism rather than committed to application configuration.
Architecture Evolution
The platform should evolve incrementally.
Phase 1
Modular Django + PostgreSQL
│
▼
Phase 2
Ledger + Reservations + Warehouse Workflows
│
▼
Phase 3
Transactional Outbox
│
▼
Phase 4
Kafka + Event Consumers
│
▼
Phase 5
CQRS Read Models
│
▼
Phase 6
Forecasting + Replenishment
│
▼
Phase 7
Advanced Integrations
│
▼
Phase 8
Selective Service Extraction
The architecture becomes more distributed only when business scale justifies it.
End-to-End Receiving Example
A complete receiving operation looks like:
Supplier Shipment Arrives
│
▼
ASN Matched
│
▼
Receiving Session Created
│
▼
Scanner Reads Barcode
│
▼
Command Sent to API
│
▼
Tenant + Warehouse Authorization
│
▼
Idempotency Check
│
▼
Database Transaction
│
├── Receipt Line Updated
├── Stock Movement Created
├── Inventory State Updated
├── PO Quantity Updated
└── Outbox Event Created
│
▼
COMMIT
│
▼
Scanner Success
│
▼
Outbox Publisher
│
▼
Kafka
│
├── Forecasting
├── Reporting
├── Procurement
└── Audit
The scanner does not wait for all downstream systems.
Only the critical transaction needs to complete before confirming the operation.
End-to-End Picking Example
Sales Order
│
▼
Availability Check
│
▼
Reservation
│
▼
Allocation
│
▼
Pick Task
│
▼
Scanner
│
▼
Confirm Pick
│
▼
Inventory Transaction
│
├── Stock Movement
├── Reservation Consumption
└── Outbox Event
│
▼
Kafka
│
├── Read Model
├── Analytics
└── Notification
The inventory mutation remains transactional.
The downstream consequences remain asynchronous.
End-to-End Transfer Example
Transfer Requested
│
▼
Availability Check
│
▼
Stock Allocated
│
▼
Dispatch
│
├── Source Inventory Reduced
├── In-Transit Position Created
└── Event Published
│
▼
Carrier / Internal Transport
│
▼
Destination Receiving
│
▼
Idempotency Check
│
▼
Transfer-In Transaction
│
├── In-Transit Reduced
├── Destination Increased
└── Event Published
│
▼
Transfer Completed
The system never needs to pretend that stock teleported from one warehouse to another.
Real-World Architectural Parallels
Flexport
The architecture reflects the same broad principle found in modern logistics platforms: shipment visibility is based on continuously changing operational events rather than a single static shipment status.
ShipBob
Modern fulfillment platforms demonstrate the importance of connecting inventory, warehouse execution, order fulfillment, and distributed inventory visibility.
Manhattan Associates
Large-scale warehouse management demonstrates why barcode-driven execution, location-aware inventory, allocation, picking, and operational correctness must be treated as one integrated system.
The goal is not to reproduce these companies.
The goal is to apply the architectural principles that make systems operating at that class of complexity possible.
What I Would Build Today
For an initial production implementation:
Application
Django
Django REST Framework
Database
PostgreSQL
Caching
Redis
Background Processing
Celery
Event Backbone
Kafka
Event Reliability
Transactional Outbox
Architecture
Modular Monolith
+ Domain Events
+ CQRS Read Models
Infrastructure
Docker
Managed PostgreSQL
Managed Kafka where practical
Observability
Structured Logging
Metrics
Distributed Tracing
I would not start by deploying:
15 microservices
+
multiple databases
+
Kafka
+
Kubernetes
before the business boundaries have proven that they need them.
Architecture is not about maximizing infrastructure.
Architecture is about preserving correctness while creating room for scale.
What I Would Explicitly Avoid
Mutable Inventory Counters as Source of Truth
inventory.quantity = 178
Avoid.
Instead:
StockMovement(
quantity=-22,
movement_type="cycle_count_adjustment"
)
Direct Database Mutation From Every Module
Avoid:
WarehouseService()
→ directly edits Procurement tables
Prefer:
Command
↓
Owning Domain
↓
Domain Event
↓
Interested Consumers
Kafka as the Inventory Database
Avoid:
Kafka = source of truth
Use:
PostgreSQL = transactional truth
Kafka = event distribution
Redis as Inventory Truth
Avoid:
Redis quantity = authoritative quantity
Redis should be disposable.
Synchronous Chains Across Domains
Avoid:
Receive
↓
Procurement API
↓
Forecasting API
↓
Notification API
↓
Reporting API
↓
Success
One downstream failure should not cause a warehouse scanner to fail.
Prefer:
Receive
↓
Transactional Commit
↓
Outbox
↓
Kafka
↓
Independent Consumers
Global Distributed Transactions
Avoid trying to make:
PostgreSQL
+
Kafka
+
Supplier API
+
Carrier API
one giant transaction.
Distributed systems fail.
The architecture should tolerate that fact.
Architecture Decision Records
Important architectural decisions should be documented explicitly.
Examples:
ADR-001
Inventory is modeled as an append-only ledger.
ADR-002
PostgreSQL remains the transactional source of truth.
ADR-003
Kafka is used for asynchronous event distribution.
ADR-004
Transactional Outbox guarantees reliable event publication.
ADR-005
Read models are derived through CQRS projections.
ADR-006
Warehouse transfers explicitly model IN_TRANSIT state.
ADR-007
Physical operations require idempotency keys.
ADR-008
The initial implementation uses a modular monolith.
ADR-009
Redis is an acceleration layer, never canonical state.
ADR-010
External integrations are asynchronous and idempotent.
These decisions are more valuable architecturally than simply listing technologies.
Architecture Quality Attributes
Nexus is optimized around several quality attributes.
| Attribute | Architectural Mechanism |
|---|---|
| Correctness | Transactional ledger + invariants |
| Auditability | Immutable movements + audit events |
| Scalability | CQRS + asynchronous consumers |
| Reliability | Outbox + retries + idempotency |
| Availability | Async downstream processing |
| Performance | Read projections + Redis |
| Security | Tenant + warehouse-scoped authorization |
| Recoverability | PostgreSQL backup + projection rebuild |
| Maintainability | Domain boundaries + modular architecture |
| Extensibility | Domain events + integration contracts |
| Observability | Correlation IDs + metrics + tracing |
The Architectural Core
Everything eventually comes back to one decision:
Physical Reality
│
▼
Business Command
│
▼
Domain Validation
│
▼
Transactional Ledger
│
▼
Commit
│
▼
Domain Event
│
▼
Kafka
│
┌───────────────┼────────────────┐
▼ ▼ ▼
Read Models Forecasting Integrations
│ │ │
▼ ▼ ▼
Dashboards Reordering Suppliers
The database transaction establishes what actually happened.
The event stream communicates that fact.
The read models make the fact easy to query.
The automation reacts to the fact.
The integrations propagate the fact.
That separation is what makes the architecture resilient.
Evolution Path
Single Warehouse
│
▼
Ledger-Based Inventory
│
▼
Reservations & Availability
│
▼
Multi-Warehouse Inventory
│
▼
Explicit In-Transit Modeling
│
▼
Warehouse Execution
│
▼
Procurement Integration
│
▼
Transactional Outbox
│
▼
Kafka Event Backbone
│
▼
CQRS Read Models
│
▼
Carrier & Supplier Integrations
│
▼
Forecasting
│
▼
Automated Replenishment
│
▼
Selective Domain Extraction
The critical insight is that the architecture does not need to become distributed on day one.
The domain boundaries and invariants need to be correct on day one.
Infrastructure can evolve as scale demands it.
What Makes Nexus SCM an Architecture
Nexus is not simply:
Django
+
PostgreSQL
+
Kafka
+
Redis
+
Celery
That is a technology stack.
The architecture is the set of decisions connecting those technologies to business requirements:
Inventory Accuracy
↓
Ledger
Operational Reliability
↓
Transactional Outbox
High-Frequency Warehouse Writes
↓
Transactional Write Model
Complex Reporting
↓
CQRS Read Models
Distributed Processing
↓
Kafka
External Failure
↓
Idempotency + Retry + DLQ
Multi-Warehouse Movement
↓
Explicit Transfer + IN_TRANSIT State
Tenant Isolation
↓
Organization + Warehouse Scope
Forecasting
↓
Movement History + Async Processing
Long-Term Scalability
↓
Modular Boundaries + Selective Extraction
That is architecture.
Key Takeaways
Nexus SCM treats the supply chain as a distributed physical system rather than a collection of CRUD tables.
The architecture is built around several foundational decisions:
- Inventory is a ledger, not a mutable number.
- Physical operations are transactional commands that produce durable business facts.
- PostgreSQL is the transactional source of truth.
- Transactional Outbox guarantees that committed business events are eventually published.
- Kafka distributes events but does not own inventory truth.
- Consumers are idempotent because duplicate delivery is expected.
- Warehouse transfers explicitly model inventory in transit.
- Reservations are commitments, not physical stock movements.
- CQRS separates operational writes from analytical reads.
- Read models are disposable and rebuildable projections.
- Redis accelerates the system but never owns canonical state.
- Celery handles asynchronous and scheduled workloads.
- Multi-tenancy and warehouse scope are enforced as architectural boundaries.
- External suppliers and carriers are treated as unreliable distributed-system participants.
- Reconciliation is part of the architecture, not an emergency procedure.
- A modular monolith provides a pragmatic starting point before selective service extraction.
- Observability, auditability, idempotency, and failure recovery are first-class architectural concerns.
The fundamental philosophy is simple:
The system should never need to guess what happened to inventory.
Every meaningful movement has a cause.
Every cause has a transaction.
Every transaction can produce an event.
Every event can be traced.
Every projection can be rebuilt.
And every warehouse, supplier, order, shipment, and forecast ultimately derives from a system that can explain where the number came from.
That is the architecture behind Nexus SCM.


