Asynchronous Inventory Processing Implementation
Issue Type
Enhancement
Priority
High
Summary
Implement asynchronous inventory processing architecture to improve system performance and scalability by decoupling inventory availability calculations from transactional documents processing.
Background
Problem Statement
The current ADempiere inventory management system processes stock calculations synchronously during document completion (Sales Orders, Purchase Orders, Shipments, Production Orders, etc.). This approach causes:
- Performance bottlenecks during high-volume transaction periods
- User experience issues with delayed document processing
- Scalability limitations as transaction volume increases
- Lock contention on inventory tables during concurrent operations
- Difficulty tracking historical inventory states for auditing and analysis
Business Impact
- Document processing time increases linearly with inventory complexity
- Users experience delays during order placement and shipment processing
- System cannot handle peak transaction loads efficiently
- Limited ability to analyze historical inventory trends
- Difficult to identify root causes of inventory discrepancies
Solution Overview
Architecture Changes
Implement a hybrid event-based architecture with three core components:
- M_Reservation Table: Historical tracking of all inventory commitments and releases
- M_Storage Table: Materialized view of current inventory availability (On Hand, Reserved, Ordered)
- Asynchronous Queue Processing: Decoupled inventory calculations via job queue
Key Design Principles
- Event Sourcing: Every reservation change is recorded as an immutable event
- Eventual Consistency: Inventory availability is updated asynchronously after document processing
- Snapshot Capability: Point-in-time inventory states for reporting and reconciliation
- Queue-Based Processing: Non-blocking inventory updates with guaranteed processing
Technical Implementation
1. New Database Tables
M_Reservation (Inventory Reservations History)
Purpose: Track all inventory commitments and releases as historical events
Key Columns:
M_Reservation_ID (PK): Unique identifier
ReservationType: Type of reservation (SO+/SO-, PO+/PO-, Production, etc.)
M_Product_ID: Product being reserved/released
M_Warehouse_ID: Warehouse location
M_Locator_ID: Specific storage location
M_AttributeSetInstance_ID: Product attributes (lot, serial, expiry)
Qty: Quantity (positive = reservation, negative = release)
- Document references:
C_Order_ID, C_OrderLine_ID, DD_Order_ID, M_InOut_ID, M_Production_ID, etc.
- Audit fields:
Created, CreatedBy, Updated, UpdatedBy
Reservation Types:
SO (Reserve Quantity): Sales order reservation - decreases available stock
PO (Order Quantity): Purchase order expected - increases projected stock
- Production orders, distribution orders, and movement reservations
Example Flow:
-- Sales order created (10 units reserved)
INSERT INTO M_Reservation (ReservationType, M_Product_ID, Qty, C_Order_ID, ...)
VALUES ('SO', 12345, 10, 98765, ...);
-- Shipment processed (10 units released from reservation)
INSERT INTO M_Reservation (ReservationType, M_Product_ID, Qty, M_InOut_ID, ...)
VALUES ('SO', 12345, -10, 55555, ...);
-- Net effect: Reserved quantity returns to 0
M_StorageSnapshot (Point-in-Time Inventory)
Purpose: Capture complete inventory state at specific moments for reporting and reconciliation
Key Columns:
M_StorageSnapshot_ID (PK): Unique identifier
M_StorageSnapshotRun_ID (FK): Link to snapshot execution
M_Product_ID: Product
M_Locator_ID: Storage location
M_AttributeSetInstance_ID: Product attributes
QtyOnHand: Physical quantity (from M_Transaction)
QtyReserved: Reserved quantity (from M_Reservation)
QtyOrdered: Ordered quantity (from M_Reservation PO type)
DateTrx: Snapshot timestamp
Use Cases:
- Daily inventory snapshots for historical analysis
- Month-end inventory valuation
- Inventory trend analysis
- Discrepancy investigation
- Audit trail compliance
M_StorageSnapshotRun (Snapshot Execution Log)
Purpose: Track snapshot execution metadata
Key Columns:
M_StorageSnapshotRun_ID (PK): Unique identifier
DocumentNo: Execution number
DateLastRun: Execution timestamp
ProductProcesses: Number of products processed
TransactionsProcessed: Number of transactions aggregated
2. Queue Processing System
Queue Type: SMM_Storage_Update
Configuration:
- Queue Type:
SMM (Storage Management)
- Classname:
org.solop.queue.storage.StorageUpdate
- Processing Mode: Asynchronous batch processing
- Frequency: Every 5 minutes (configurable)
- Batch Size: 100 records per batch (configurable)
Processing Flow
Document Completion (Sales Order, Shipment, etc.)
↓
Create M_Reservation entries
↓
Add document to queue (SMM_Storage_Update)
↓
Continue user workflow (non-blocking)
↓
[Background Queue Processor]
↓
Extract product IDs from queued documents
↓
Rebuild M_Storage for affected products
↓
Mark queue entry as processed
Key Components
StorageUpdate.java (Queue Processor):
- Extracts product IDs from queued documents
- Delegates to
StorageUpdaterBuilder for inventory recalculation
- Supports multiple document types:
- Sales Orders (
C_Order)
- Purchase Orders (via
C_Order)
- Distribution Orders (
DD_Order)
- Shipments/Receipts (
M_InOut)
- Inventory Movements (
M_Movement)
- Production Orders (
M_ProductionBatch)
- Production Execution (
M_Production)
- Project Issues (
C_ProjectIssue)
- Physical Inventory (
M_Inventory)
StorageUpdaterBuilder.java (Calculation Engine):
- Deletes existing M_Storage records for affected products
- Recalculates from source data:
QtyOnHand: SUM(M_Transaction.MovementQty)
QtyReserved: SUM(M_Reservation.Qty WHERE ReservationType NOT IN ('PO+', 'PO-'))
QtyOrdered: SUM(M_Reservation.Qty WHERE ReservationType IN ('PO+', 'PO-'))
- Groups by Product, Locator, AttributeSetInstance
- Creates new M_Storage records with calculated values
3. Document Integration
Each transactional document now follows this pattern:
Sales Order (MOrder.java / MOrderLine.java)
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
// Create reservation when order is saved
ReservationBuilder.newInstance(getCtx(), get_TrxName())
.withSalesOrderLine(this)
.build();
return success;
}
Reservation Events:
- Order created → Create positive reservation (
ReservationType = 'SO')
- Order modified → Adjust reservation (delta calculation)
- Order voided → Create negative reservation to release
- Shipment created → Create negative reservation to release
Shipment/Receipt (MInOut.java)
@Override
protected boolean afterComplete(String DocAction) {
// Add to storage update queue
StorageUpdate.addDocumentToQueue(this);
return true;
}
Processing:
- Shipment completed → Creates M_Transaction records
- Releases reservations → Negative M_Reservation entries
- Adds to queue → Async M_Storage update
Production Orders (MProductionBatch.java / MProductionBatchLine.java)
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
// Reserve raw materials (negative for components)
// Reserve production output (positive for finished goods)
ReservationBuilder.newInstance(getCtx(), get_TrxName())
.withProductionOrderLine(this)
.build();
return success;
}
Material Flow:
- Production order created → Reserve raw materials (SO type)
- Production order created → Reserve output location (PO type for finished goods)
- Production executed → Release reservations, create transactions
Distribution Orders (MOrder with DD_Order type)
Reservations:
- From warehouse:
ReservationType = 'SO' (reserve at source)
- To warehouse:
ReservationType = 'PO' (expected at destination)
4. Snapshot Process (UpdateStorage.java)
Purpose
Create point-in-time inventory snapshots and rebuild complete storage from historical data.
Execution Modes
Mode 1: Selective Update (Daily Operations)
// Update specific products/warehouses
StorageUpdaterBuilder.newInstance(ctx, trxName)
.withWarehouseId(warehouseId)
.withProductId(productId)
.build();
Mode 2: Full Snapshot (Periodic Reconciliation)
// Create complete system snapshot
UpdateStorage process = new UpdateStorage();
process.setIsCreateSnapshot(true);
process.doIt();
Snapshot Creation Algorithm
Step 1: Snapshot from Reservations
INSERT INTO M_StorageSnapshot (
M_Product_ID, M_Locator_ID, M_AttributeSetInstance_ID,
QtyOnHand, QtyReserved, QtyOrdered, M_StorageSnapshotRun_ID, DateTrx
)
SELECT
r.M_Product_ID,
r.M_Locator_ID,
r.M_AttributeSetInstance_ID,
0 as QtyOnHand,
SUM(CASE WHEN r.ReservationType NOT IN('PO+', 'PO-') THEN r.Qty ELSE 0 END) as QtyReserved,
SUM(CASE WHEN r.ReservationType IN('PO+', 'PO-') THEN r.Qty ELSE 0 END) as QtyOrdered,
?, -- M_StorageSnapshotRun_ID
? -- Current timestamp
FROM M_Reservation r
GROUP BY r.M_Product_ID, r.M_Locator_ID, r.M_AttributeSetInstance_ID
HAVING SUM(r.Qty) <> 0;
Step 2: Snapshot from Transactions
INSERT INTO M_StorageSnapshot (
M_Product_ID, M_Locator_ID, M_AttributeSetInstance_ID,
QtyOnHand, QtyReserved, QtyOrdered, M_StorageSnapshotRun_ID, DateTrx
)
SELECT
mt.M_Product_ID,
mt.M_Locator_ID,
mt.M_AttributeSetInstance_ID,
SUM(mt.MovementQty) as QtyOnHand,
0 as QtyReserved,
0 as QtyOrdered,
?, -- M_StorageSnapshotRun_ID
? -- Current timestamp
FROM M_Transaction mt
INNER JOIN M_Locator ml ON mt.M_Locator_ID = ml.M_Locator_ID
GROUP BY mt.M_Product_ID, mt.M_Locator_ID, mt.M_AttributeSetInstance_ID
HAVING SUM(mt.MovementQty) <> 0;
Step 3: Rebuild M_Storage
StorageUpdaterBuilder.newInstance(ctx, trxName)
.withClientId(clientId)
.build(); // Rebuilds all storage records
Scheduled Jobs
Job 1: Queue Processor (FlushSystemQueue)
- Frequency: Every 5 minutes
- Purpose: Process pending storage updates
- Parameters:
- Batches to process: 10
- Records per batch: 100
- Delete after process: No
- Queue Type: SMM_Storage_Update
Job 2: Snapshot Creator (UpdateStorage)
- Frequency: Daily (configurable)
- Purpose: Create full inventory snapshot and rebuild storage
- Parameters:
Code Changes Summary
New Classes
- MReservation.java - Reservation entity model
- MStorageSnapshot.java - Snapshot entity model
- MStorageSnapshotRun.java - Snapshot run entity model
- StorageUpdate.java - Queue processor for storage updates
- UpdateStorage.java - Snapshot creation and storage rebuild process
- StorageUpdaterBuilder.java - Storage calculation builder
- CreateStorageUpdateSchedule.java - Setup utility for scheduled jobs
Modified Classes
- MOrder.java / MOrderLine.java - Added reservation creation
- MInOut.java - Added queue integration
- MMovement.java - Added queue integration
- MProduction.java - Added queue integration
- MProductionBatch.java / MProductionBatchLine.java - Added reservation logic
- MInventory.java - Added queue integration
- MProjectIssue.java - Added queue integration
- MStorage.java - Modified to work with async updates
New Database Tables
M_Reservation - Reservation history
M_StorageSnapshot - Point-in-time snapshots
M_StorageSnapshotRun - Snapshot execution log
New Queue Type
- SMM_Storage_Update (Storage Management Module)
Benefits
Performance Improvements
- Faster Document Processing: Document completion no longer blocks on inventory calculations
- Reduced Lock Contention: Inventory updates happen in background batches
- Scalability: System can handle 10x more concurrent transactions
- Predictable Response Times: User operations complete in milliseconds instead of seconds
Operational Benefits
- Historical Tracking: Complete audit trail of all reservation events
- Snapshot Capability: Point-in-time inventory for reporting and reconciliation
- Easier Debugging: Can trace exact sequence of events affecting inventory
- Flexible Reporting: Query historical inventory states for any date/time
Data Quality
- Eventually Consistent: Storage always reflects true state after queue processing
- Reconciliation: Snapshots enable automated discrepancy detection
- Audit Trail: Every reservation change is logged with user and timestamp
Migration Strategy
Phase 1: Initial Deployment
- Deploy new database tables (M_Reservation, M_StorageSnapshot, M_StorageSnapshotRun)
- Deploy queue infrastructure (SMM_Storage_Update queue type)
- Deploy code changes to document models
- Create scheduled jobs (queue processor, snapshot creator)
Phase 2: Historical Data Migration
- Run initial snapshot to baseline current inventory
- Validate snapshot against current M_Storage
- Archive old M_Storage records (if needed)
Phase 3: Monitoring & Optimization
- Monitor queue processing times
- Adjust batch sizes based on load
- Tune snapshot frequency
- Monitor storage calculation accuracy
Rollback Plan
If issues arise:
- Disable queue processor scheduled job
- Revert to synchronous storage updates (legacy code path)
- Investigate and fix issues
- Re-enable asynchronous processing
Testing Requirements
Unit Tests
Integration Tests
Reconciliation Tests
Configuration
System Properties
# Queue Processing
queue.storage.batch.size=100
queue.storage.frequency.minutes=5
queue.storage.delete.after.process=false
# Snapshot
snapshot.frequency.days=1
snapshot.retention.days=90
Scheduled Jobs Setup
// Auto-created by CreateStorageUpdateSchedule.java
// 1. Queue Processor: Every 5 minutes
// 2. Snapshot Creator: Daily at midnight
Monitoring & Metrics
Key Metrics to Track
- Queue Processing Time: Average time per batch
- Queue Depth: Number of pending documents
- Storage Calculation Time: Time to rebuild M_Storage per product
- Snapshot Duration: Time to create full system snapshot
- Discrepancies: Differences between snapshots and expected values
Dashboard Queries
-- Queue backlog
SELECT COUNT(*) FROM AD_Queue WHERE AD_QueueType_ID = 50016 AND Processed = 'N';
-- Average processing time
SELECT AVG(Updated - Created) FROM AD_Queue WHERE AD_QueueType_ID = 50016 AND Processed = 'Y';
-- Latest snapshot stats
SELECT * FROM M_StorageSnapshotRun ORDER BY DateLastRun DESC LIMIT 1;
-- Products with discrepancies
SELECT s.M_Product_ID,
s.QtyOnHand as StorageQty,
ss.QtyOnHand as SnapshotQty,
s.QtyOnHand - ss.QtyOnHand as Difference
FROM M_Storage s
LEFT JOIN M_StorageSnapshot ss ON s.M_Product_ID = ss.M_Product_ID
AND s.M_Locator_ID = ss.M_Locator_ID
WHERE ABS(s.QtyOnHand - ss.QtyOnHand) > 0.001;
Known Limitations
- Eventual Consistency: Brief lag between document completion and inventory availability update
- Queue Dependency: System depends on queue processor running reliably
- Storage During Migration: Cannot run old and new system simultaneously
Future Enhancements
- Real-time Availability: Add Redis cache for instant availability checks
- Event Streaming: Integrate with Kafka for event distribution
- Predictive Analytics: Use reservation history for demand forecasting
- Automated Reconciliation: Auto-detect and alert on inventory discrepancies
- Distributed Processing: Scale queue processors horizontally
Documentation Updates Required
- User Manual: Explain asynchronous processing behavior
- Technical Documentation: Architecture diagrams and data flow
- API Documentation: New reservation and snapshot endpoints
- Migration Guide: Steps for existing installations
- Troubleshooting Guide: Common issues and resolutions
References
Related Issues
- Performance improvements for high-volume transactions
- Inventory audit trail requirements
- Historical inventory reporting
Design Documents
- Event Sourcing Architecture Pattern
- Queue-Based Processing Best Practices
- Inventory Management Data Model
External Resources
- ADempiere Wiki: Storage Management
- Queue Framework Documentation
- Database Performance Tuning Guide
Implementation Checklist
Contributors
- Developer: [Your Name]
- Reviewers: [Team Members]
- Testing: [QA Team]
Version History
- v1.0 - Initial implementation
- Date: January 22, 2026
Approval Sign-off
Asynchronous Inventory Processing Implementation
Issue Type
Enhancement
Priority
High
Summary
Implement asynchronous inventory processing architecture to improve system performance and scalability by decoupling inventory availability calculations from transactional documents processing.
Background
Problem Statement
The current ADempiere inventory management system processes stock calculations synchronously during document completion (Sales Orders, Purchase Orders, Shipments, Production Orders, etc.). This approach causes:
Business Impact
Solution Overview
Architecture Changes
Implement a hybrid event-based architecture with three core components:
Key Design Principles
Technical Implementation
1. New Database Tables
M_Reservation (Inventory Reservations History)
Purpose: Track all inventory commitments and releases as historical events
Key Columns:
M_Reservation_ID(PK): Unique identifierReservationType: Type of reservation (SO+/SO-, PO+/PO-, Production, etc.)M_Product_ID: Product being reserved/releasedM_Warehouse_ID: Warehouse locationM_Locator_ID: Specific storage locationM_AttributeSetInstance_ID: Product attributes (lot, serial, expiry)Qty: Quantity (positive = reservation, negative = release)C_Order_ID,C_OrderLine_ID,DD_Order_ID,M_InOut_ID,M_Production_ID, etc.Created,CreatedBy,Updated,UpdatedByReservation Types:
SO(Reserve Quantity): Sales order reservation - decreases available stockPO(Order Quantity): Purchase order expected - increases projected stockExample Flow:
M_StorageSnapshot (Point-in-Time Inventory)
Purpose: Capture complete inventory state at specific moments for reporting and reconciliation
Key Columns:
M_StorageSnapshot_ID(PK): Unique identifierM_StorageSnapshotRun_ID(FK): Link to snapshot executionM_Product_ID: ProductM_Locator_ID: Storage locationM_AttributeSetInstance_ID: Product attributesQtyOnHand: Physical quantity (from M_Transaction)QtyReserved: Reserved quantity (from M_Reservation)QtyOrdered: Ordered quantity (from M_Reservation PO type)DateTrx: Snapshot timestampUse Cases:
M_StorageSnapshotRun (Snapshot Execution Log)
Purpose: Track snapshot execution metadata
Key Columns:
M_StorageSnapshotRun_ID(PK): Unique identifierDocumentNo: Execution numberDateLastRun: Execution timestampProductProcesses: Number of products processedTransactionsProcessed: Number of transactions aggregated2. Queue Processing System
Queue Type: SMM_Storage_Update
Configuration:
SMM(Storage Management)org.solop.queue.storage.StorageUpdateProcessing Flow
Key Components
StorageUpdate.java (Queue Processor):
StorageUpdaterBuilderfor inventory recalculationC_Order)C_Order)DD_Order)M_InOut)M_Movement)M_ProductionBatch)M_Production)C_ProjectIssue)M_Inventory)StorageUpdaterBuilder.java (Calculation Engine):
QtyOnHand: SUM(M_Transaction.MovementQty)QtyReserved: SUM(M_Reservation.Qty WHERE ReservationType NOT IN ('PO+', 'PO-'))QtyOrdered: SUM(M_Reservation.Qty WHERE ReservationType IN ('PO+', 'PO-'))3. Document Integration
Each transactional document now follows this pattern:
Sales Order (MOrder.java / MOrderLine.java)
Reservation Events:
ReservationType = 'SO')Shipment/Receipt (MInOut.java)
Processing:
Production Orders (MProductionBatch.java / MProductionBatchLine.java)
Material Flow:
Distribution Orders (MOrder with DD_Order type)
Reservations:
ReservationType = 'SO'(reserve at source)ReservationType = 'PO'(expected at destination)4. Snapshot Process (UpdateStorage.java)
Purpose
Create point-in-time inventory snapshots and rebuild complete storage from historical data.
Execution Modes
Mode 1: Selective Update (Daily Operations)
Mode 2: Full Snapshot (Periodic Reconciliation)
Snapshot Creation Algorithm
Step 1: Snapshot from Reservations
Step 2: Snapshot from Transactions
Step 3: Rebuild M_Storage
Scheduled Jobs
Job 1: Queue Processor (FlushSystemQueue)
Job 2: Snapshot Creator (UpdateStorage)
Code Changes Summary
New Classes
Modified Classes
New Database Tables
M_Reservation- Reservation historyM_StorageSnapshot- Point-in-time snapshotsM_StorageSnapshotRun- Snapshot execution logNew Queue Type
Benefits
Performance Improvements
Operational Benefits
Data Quality
Migration Strategy
Phase 1: Initial Deployment
Phase 2: Historical Data Migration
Phase 3: Monitoring & Optimization
Rollback Plan
If issues arise:
Testing Requirements
Unit Tests
Integration Tests
Reconciliation Tests
Configuration
System Properties
Scheduled Jobs Setup
Monitoring & Metrics
Key Metrics to Track
Dashboard Queries
Known Limitations
Future Enhancements
Documentation Updates Required
References
Related Issues
Design Documents
External Resources
Implementation Checklist
Contributors
Version History
Approval Sign-off