This document presents a robust low-level design for an inventory reservation system that handles concurrent requests, ensures data consistency across distributed services, and provides scalable reservation management with conflict resolution mechanisms.
1. Problem Statement & Requirements
Clarifying questions
-
What is the scale of inventory items and reservations we're expected to handle?
- Is this a system for a global e-commerce platform with millions of SKUs and concurrent reservations, or a smaller-scale internal system?
-
What are the consistency requirements for inventory reservations?
- Should we guarantee strong consistency (e.g., immediate availability updates) or can eventual consistency be acceptable?
-
What are the key entities and their relationships in the system?
- Is inventory tied to specific locations, warehouses, or SKUs? Do reservations have expiration times?
-
What is the read/write ratio for inventory data?
- Is the system primarily used for frequent inventory checks or more for placing reservations?
-
How sensitive is the system to latency?
- Are reservations expected to be processed in under 100ms for a good user experience?
-
What authentication and authorization model is in place?
- Are reservations made by authenticated users or via API keys?
-
Are there any external integrations or systems that the inventory system must interact with?
- For example, order management systems, warehouse systems, or third-party suppliers?
Functional requirements
- Allow users to reserve inventory items for a specific duration (e.g., 15 minutes).
- Prevent over-reservation of inventory items.
- Automatically release reservations that expire.
- Allow users to explicitly release reservations before expiration.
- Support querying current inventory levels and reservation status for items.
- Allow concurrent reservations from multiple users without race conditions.
Non-functional requirements
-
Latency targets:
- p50: <50ms for inventory queries and reservations
- p95: <100ms
- p99: <200ms
-
Throughput:
- Support at least 10,000 concurrent reservations per second.
-
Availability:
- 99.9% uptime for inventory reservation service.
Constraints and assumptions
- Assumption: Inventory items have a fixed stock count.
- Assumption: Reservations are tied to a specific user or session.
- Assumption: The system is designed for high-concurrency, low-latency operations.
- Constraint: Inventory items cannot go below zero in stock.
Out of scope
- Inventory replenishment or restocking logic.
- Multi-warehouse inventory management (inventory per location).
- Advanced reservation policies (e.g., priority-based reservations or pre-ordering).
- Integration with external systems like ERP, warehouse management systems, or order fulfillment services.
- User session management and authentication (handled by upstream services).
- Real-time inventory synchronization with external sources.
2. API Design
API Style Selection
For inventory reservation system, I'll select RESTful API with JSON payloads for the following reasons:
- Familiar to most developers and tooling ecosystem
- Well-suited for CRUD operations on inventory items and reservations
- Supports standard HTTP methods (GET, POST, PUT, DELETE) for clear semantics
- Easy to document and test with standard tools like Postman or curl
- Good balance between simplicity and extensibility
Endpoint Definitions
| Method | Path | Description | Auth Required |
|---|---|---|---|
| POST | /v1/inventory/reserve | Reserve inventory items for a specific order | Yes |
| GET | /v1/inventory/reserve/{reservationId} | Get reservation details by ID | Yes |
| PUT | /v1/inventory/reserve/{reservationId} | Update reservation details (extend duration, change items) | Yes |
| DELETE | /v1/inventory/reserve/{reservationId} | Release reservation | Yes |
| GET | /v1/inventory/items | List all inventory items with stock levels | Yes |
| GET | /v1/inventory/items/{itemId} | Get specific inventory item details | Yes |
| POST | /v1/inventory/items | Create new inventory item | Admin Only |
| PUT | /v1/inventory/items/{itemId} | Update existing inventory item | Admin Only |
| DELETE | /v1/inventory/items/{itemId} | Remove inventory item (soft delete) | Admin Only |
Request and Response Schemas
Reserve Inventory Request
{
"orderId": "string",
"items": [
{
"itemId": "string",
"quantity": "integer"
}
],
"reservationDurationMinutes": "integer",
"customerId": "string"
}
Reserve Inventory Response
{
"reservationId": "string",
"orderId": "string",
"items": [
{
"itemId": "string",
"quantity": "integer",
"status": "reserved|failed"
}
],
"reservationExpiryTime": "datetime",
"status": "success|partial_success|failed"
}
Get Reservation Response
{
"reservationId": "string",
"orderId": "string",
"customerId": "string",
"items": [
{
"itemId": "string",
"quantity": "integer"
}
],
"status": "active|expired|released",
"reservationExpiryTime": "datetime",
"createdAt": "datetime",
"updatedAt": "datetime"
}
Create Inventory Item Request
{
"itemId": "string",
"name": "string",
"description": "string",
"category": "string",
"unitPrice": "number",
"availableStock": "integer"
}
Create Inventory Item Response
{
"itemId": "string",
"name": "string",
"description": "string",
"category": "string",
"unitPrice": "number",
"availableStock": "integer",
"createdAt": "datetime"
}
HTTP Status Codes and Error Response Format
| Status Code | Description |
|---|---|
| 200 | Success (GET, PUT) |
| 201 | Created (POST) |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Missing or invalid auth |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Reservation conflict or item already exists |
| 422 | Unprocessable Entity - Validation errors |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
Standardized error response format:
{
"error": {
"code": "string",
"message": "string",
"details": "object",
"timestamp": "datetime"
}
}
API Versioning Strategy
Versioning will be done through the path prefix: /v1/... with future versions as /v2/.... This approach:
- Is simple to implement and understand
- Allows for clean separation of concerns between versions
- Supports gradual migration of clients to newer versions
Pagination Design
Cursor-based pagination will be used for listing endpoints:
{
"items": [...],
"nextCursor": "string",
"hasMore": "boolean"
}
Justification:
- Cursor-based pagination is more stable than offset-based when data changes
- It provides better performance for large datasets
- It's more predictable in handling concurrent modifications
Authentication and Authorization
Authentication will use JWT tokens with OAuth2 flow for third-party integrations. Authorization model will be Role-Based Access Control (RBAC):
customerrole - can only access their own reservationsadminrole - full CRUD access to inventory items and reservationsservicerole - can create reservations for system operations
Rate Limiting
Rate limiting will be implemented per endpoint:
/v1/inventory/reserve: 100 requests/minute per customer/v1/inventory/items: 50 requests/minute per admin/v1/inventory/items/{itemId}: 200 requests/minute per customer
Implementation strategy:
- Token bucket algorithm for rate limiting
- Distributed implementation using Redis to handle multi-instance deployments
Idempotency Design
For mutation endpoints, idempotency keys will be supported:
POST /v1/inventory/reserve
X-Idempotency-Key: <unique-key>
Implementation:
- Store idempotency key with reservation ID in Redis
- If same key is used, return the existing reservation
- TTL of 5 minutes for idempotency keys to prevent memory leaks
Example Request with Idempotency
POST /v1/inventory/reserve
Content-Type: application/json
Authorization: Bearer <jwt>
X-Idempotency-Key: order-1234567890
{
"orderId": "order-1234567890",
"items": [
{
"itemId": "item-123",
"quantity": 2
}
],
"reservationDurationMinutes": 15,
"customerId": "customer-abc"
}
3. Data Model Design
Entity-Relationship Diagram
erDiagram
PRODUCT {
string id PK
string name
string description
decimal price
integer stock_quantity
timestamp created_at
timestamp updated_at
boolean is_active
}
INVENTORY_ITEM {
string id PK
string product_id FK
integer available_quantity
integer reserved_quantity
timestamp last_updated
string location
}
RESERVATION {
string id PK
string product_id FK
string inventory_item_id FK
integer quantity
timestamp reserved_at
timestamp expires_at
string status
string order_id
string user_id
}
ORDER {
string id PK
string user_id
string status
timestamp created_at
timestamp updated_at
}
PRODUCT ||--o{ INVENTORY_ITEM : "has"
INVENTORY_ITEM ||--o{ RESERVATION : "reserves"
PRODUCT ||--o{ RESERVATION : "reserves"
ORDER ||--o{ RESERVATION : "creates"
Table Schema Definitions
Product Table
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
INDEX idx_products_name (name),
INDEX idx_products_active (is_active),
INDEX idx_products_price (price)
);
InventoryItem Table
CREATE TABLE inventory_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
available_quantity INTEGER NOT NULL DEFAULT 0,
reserved_quantity INTEGER NOT NULL DEFAULT 0,
last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
location VARCHAR(100),
INDEX idx_inventory_items_product (product_id),
INDEX idx_inventory_items_location (location),
INDEX idx_inventory_items_available (available_quantity),
CONSTRAINT chk_available_ge_reserved CHECK (available_quantity >= reserved_quantity)
);
Reservation Table
CREATE TABLE reservations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
inventory_item_id UUID NOT NULL REFERENCES inventory_items(id) ON DELETE CASCADE,
quantity INTEGER NOT NULL,
reserved_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
order_id UUID REFERENCES orders(id),
user_id UUID NOT NULL,
INDEX idx_reservations_product (product_id),
INDEX idx_reservations_inventory_item (inventory_item_id),
INDEX idx_reservations_status (status),
INDEX idx_reservations_expires_at (expires_at),
INDEX idx_reservations_user_product (user_id, product_id),
CONSTRAINT chk_reservation_quantity_positive CHECK (quantity > 0)
);
Order Table
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'CREATED',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
INDEX idx_orders_user (user_id),
INDEX idx_orders_status (status),
INDEX idx_orders_created_at (created_at)
);
Primary Key Strategy
UUIDs for all entities - chosen over auto-increment for:
- Global uniqueness across distributed systems
- Preventing enumeration attacks (no sequential IDs)
- Better sharding capabilities with UUID v1 timestamps
- Decoupling from database internals
Index Design
| Index Name | Fields | Type | Purpose |
|---|---|---|---|
| idx_products_name | name | Standard | Fast product lookups by name |
| idx_products_active | is_active | Standard | Efficient filtering of active products |
| idx_inventory_items_product | product_id | Standard | Fast inventory lookups by product |
| idx_inventory_items_location | location | Standard | Location-based inventory queries |
| idx_reservations_product | product_id | Standard | Fast reservation lookups by product |
| idx_reservations_inventory_item | inventory_item_id | Standard | Fast reservation lookups by inventory item |
| idx_reservations_status | status | Standard | Efficient status-based filtering |
| idx_reservations_expires_at | expires_at | Standard | Fast cleanup of expired reservations |
| idx_reservations_user_product | user_id, product_id | Composite | Fast user-specific reservation queries |
Data Partitioning / Sharding Strategy
Shard by Product Category or Location:
- For high-volume products, partition by product category (e.g., electronics, clothing)
- For geographically distributed inventory, shard by location/region
- Use consistent hashing for even distribution across shards
Soft Delete vs. Hard Delete Decision
Soft Delete - Active/Inactive Status Pattern:
- Products and inventory items marked as inactive rather than deleted
- Reasoning:
- Historical analytics and audit requirements
- Prevents data loss during accidental deletions
- Maintains referential integrity in reservation system
- Allows for easy reactivation of products
Audit Trail Design
Audit Log Table:
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
operation VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id UUID NOT NULL,
user_id UUID,
changes JSONB,
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_logs_entity (entity_type, entity_id),
INDEX idx_audit_logs_timestamp (timestamp),
INDEX idx_audit_logs_user (user_id)
);
Data Archival Strategy and Retention Policy
Archival Strategy:
- Products with zero stock and no recent activity (30+ days) marked as inactive
- Reservations older than 7 days with status 'EXPIRED' or 'CANCELLED' archived to historical tables
- Historical reservation data retained for 90 days for compliance and analytics
Retention Policy:
- Active products: Never expire
- Inactive products: Retain for 365 days before permanent deletion
- Reservation history: Keep for 90 days in active storage, move to cold storage after that
4. Class Design & Service Layer
UML Class Diagram
classDiagram
class InventoryService {
+reserveItems(List~ReservationRequest~ requests)
+releaseReservation(String reservationId)
+extendReservation(String reservationId, Duration duration)
+getReservationStatus(String reservationId)
}
class Reservation {
-String id
-List~ReservationItem~
-ReservationStatus status
-Instant createdAt
-Instant expiresAt
-String userId
+extend(Duration duration)
+release()
+isExpired()
}
class ReservationItem {
-String productId
-Integer quantity
-Integer availableStock
}
class Product {
-String id
-String name
-Integer availableStock
-Integer reservedStock
-Instant lastUpdated
}
class InventoryManager {
+allocateStock(List~ReservationItem~ items)
+releaseStock(List~ReservationItem~ items)
+updateStock(String productId, Integer delta)
}
class EventPublisher {
+publish(ReservationEvent event)
}
class ReservationEvent {
-String reservationId
-ReservationStatus status
-List~ReservationItem~
}
class ReservationRepository {
+save(Reservation reservation)
+findById(String id)
+findByUserIdAndStatus(String userId, ReservationStatus status)
+delete(Reservation reservation)
}
class StockUpdateService {
+updateStock(String productId, Integer delta)
}
class ReservationValidator {
+validate(ReservationRequest request)
+validateStockAvailability(List~ReservationItem~ items)
}
class ReservationStatus {
PENDING
CONFIRMED
EXPIRED
RELEASED
}
class ReservationRequest {
-String userId
-List~ReservationItem~
-Duration validityPeriod
}
InventoryService --> Reservation : creates
InventoryService --> ReservationRepository : persists
InventoryService --> EventPublisher : publishes events
InventoryService --> StockUpdateService : updates stock
InventoryService --> ReservationValidator : validates
Reservation --> ReservationItem : contains
Reservation --> ReservationEvent : emits
Product --> InventoryManager : managed by
StockUpdateService --> Product : updates
ReservationRepository --> Reservation : manages
ReservationValidator --> ReservationRequest : validates
class TransactionManager {
+beginTransaction()
+commit()
+rollback()
}
InventoryService --> TransactionManager : manages transactions
State Machine Diagram for Reservation
stateDiagram-v2
[*] --> PENDING
PENDING --> CONFIRMED : stock allocated
PENDING --> EXPIRED : timeout
CONFIRMED --> RELEASED : manual release
CONFIRMED --> EXPIRED : timeout
RELEASED --> [*]
EXPIRED --> [*]
Service Class Structure and Responsibilities
The InventoryService is the primary orchestrator for all reservation operations. It handles:
- Reservation creation and validation
- Stock allocation and release
- Reservation lifecycle management (expiration, release)
- Event publishing for audit and async processing
The StockUpdateService encapsulates all stock modification logic with proper concurrency control.
Interface and Abstract Class Design
public interface ReservationRepository {
Reservation save(Reservation reservation);
Optional<Reservation> findById(String id);
List<Reservation> findByUserIdAndStatus(String userId, ReservationStatus status);
void delete(Reservation reservation);
}
public interface EventPublisher {
void publish(ReservationEvent event);
}
public enum ReservationStatus {
PENDING, CONFIRMED, EXPIRED, RELEASED
}
public interface TransactionManager {
void beginTransaction();
void commit();
void rollback();
}
Business Logic Encapsulation Pattern
Using the Strategy Pattern for handling different reservation validation scenarios:
public interface ReservationValidationStrategy {
ValidationResult validate(ReservationRequest request);
}
@Component
public class StockAvailabilityValidationStrategy implements ReservationValidationStrategy {
private final ProductRepository productRepository;
@Override
public ValidationResult validate(ReservationRequest request) {
List<ReservationItem> items = request.getItems();
for (ReservationItem item : items) {
Product product = productRepository.findById(item.getProductId())
.orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
if (product.getAvailableStock() < item.getQuantity()) {
return ValidationResult.failure("Insufficient stock for product " + item.getProductId());
}
}
return ValidationResult.success();
}
}
public class ReservationValidator {
private final List<ReservationValidationStrategy> strategies;
public ValidationResult validate(ReservationRequest request) {
for (ReservationValidationStrategy strategy : strategies) {
ValidationResult result = strategy.validate(request);
if (!result.isSuccess()) {
return result;
}
}
return ValidationResult.success();
}
}
Critical-Path Method Implementation
@Transactional
public String reserveItems(List<ReservationRequest> requests) {
// Validate all requests
for (ReservationRequest request : requests) {
ValidationResult validation = reservationValidator.validate(request);
if (!validation.isSuccess()) {
throw new ValidationException(validation.getMessage());
}
}
// Create reservation with PENDING status
String reservationId = UUID.randomUUID().toString();
Instant now = Instant.now();
Instant expiresAt = now.plus(requests.get(0).getValidityPeriod());
Reservation reservation = new Reservation();
reservation.setId(reservationId);
reservation.setUserId(requests.get(0).getUserId());
reservation.setStatus(ReservationStatus.PENDING);
reservation.setCreatedAt(now);
reservation.setExpiresAt(expiresAt);
// Prepare items for stock allocation
List<ReservationItem> items = new ArrayList<>();
for (ReservationRequest request : requests) {
for (ReservationItem item : request.getItems()) {
items.add(new ReservationItem(
item.getProductId(),
item.getQuantity(),
getProductStock(item.getProductId())
));
}
}
// Allocate stock with pessimistic locking
try {
// Lock products for update to prevent race conditions
List<Product> lockedProducts = new ArrayList<>();
for (ReservationItem item : items) {
Product product = productRepository.lockById(item.getProductId());
if (product.getAvailableStock() < item.getQuantity()) {
throw new InsufficientStockException(
"Product " + item.getProductId() + " has insufficient stock");
}
lockedProducts.add(product);
}
// Update product stock and reservation items
for (ReservationItem item : items) {
Product product = lockedProducts.stream()
.filter(p -> p.getId().equals(item.getProductId()))
.findFirst()
.orElseThrow();
product.setReservedStock(product.getReservedStock() + item.getQuantity());
product.setAvailableStock(product.getAvailableStock() - item.getQuantity());
product.setLastUpdated(Instant.now());
// Update in DB
productRepository.save(product);
// Add to reservation item
item.setAvailableStock(product.getAvailableStock());
}
reservation.setItems(items);
reservation.setStatus(ReservationStatus.CONFIRMED);
// Save reservation
reservationRepository.save(reservation);
// Publish event for async processing and audit trail
ReservationEvent event = new ReservationEvent(
reservationId,
ReservationStatus.CONFIRMED,
items
);
eventPublisher.publish(event);
return reservationId;
} catch (Exception e) {
// Rollback stock changes if any failed
rollbackStockChanges(items);
throw new ReservationException("Failed to reserve items", e);
}
}
Transaction Boundary Design
Each reservation operation is wrapped in a single database transaction:
- Reservation creation: Includes validation, stock allocation, and persistence
- Stock updates: Must happen atomically with reservation creation
- Event publishing: Happens within same transaction to ensure consistency
Service Interface Contracts
public interface InventoryService {
String reserveItems(List<ReservationRequest> requests);
void releaseReservation(String reservationId);
void extendReservation(String reservationId, Duration duration);
ReservationStatus getReservationStatus(String reservationId);
}
Saga / Orchestration Pattern
For distributed transactions, we use a Saga pattern with local coordination:
- Reservation creation is a single-phase saga
- Stock allocation and reservation persistence happen in same transaction
- Events are published to trigger downstream processes (e.g., order creation, notifications)
Domain Object / Value Object Design
public class Reservation {
private String id;
private List<ReservationItem> items;
private ReservationStatus status;
private Instant createdAt;
private Instant expiresAt;
private String userId;
public void extend(Duration duration) {
this.expiresAt = this.expiresAt.plus(duration);
}
public void release() {
this.status = ReservationStatus.RELEASED;
}
public boolean isExpired() {
return Instant.now().isAfter(this.expiresAt);
}
}
public class ReservationItem {
private String productId;
private Integer quantity;
private Integer availableStock;
// Getters and setters
}
public class ReservationRequest {
private String userId;
private List<ReservationItem> items;
private Duration validityPeriod;
// Getters and setters
}
5. Concurrency & Thread Safety
Concurrency Model
The system uses a thread-safe, asynchronous processing model with the following characteristics:
- Thread Pool: Uses a fixed-size thread pool (e.g., 100 threads) for handling concurrent inventory reservation requests, leveraging Java's
ExecutorServicewith a custom thread factory. - Async/Await Pattern: Asynchronous processing is implemented using
CompletableFutureand reactive streams for non-blocking I/O operations. - Event Loop: For high-concurrency scenarios, the system leverages Reactor or Vert.x for event-driven, non-blocking I/O processing.
Race Condition Prevention Strategy
Race conditions in inventory reservation are prevented through:
- Optimistic Locking: Each inventory item includes a version field (e.g.,
version: int) that is validated during updates. - Atomic Operations: Critical sections like inventory decrement are implemented using atomic operations or database-level atomic updates.
- Database-Level Constraints: Unique constraints and check constraints on inventory tables prevent concurrent over-reservation.
Optimistic vs. Pessimistic Locking
Optimistic Locking is chosen over Pessimistic Locking for the following reasons:
- High Concurrency: The system handles a large number of concurrent reservation requests, and pessimistic locking would cause significant contention and blocking.
- Low Conflict Rate: Inventory reservations are typically for distinct items or time windows, reducing the likelihood of conflicts.
- Scalability: Optimistic locking allows for better horizontal scaling and avoids holding database locks for extended periods.
- Performance: Reduces lock wait times, improving throughput and reducing latency.
Deadlock Prevention Strategy
Deadlock prevention is implemented through:
- Lock Ordering: All inventory reservation operations acquire locks in a consistent order (e.g., by item ID or SKU).
- Timeout Strategy: Database transactions are configured with a timeout (e.g., 5 seconds) to prevent indefinite lock waits.
- Retry Logic: Failed transactions due to deadlocks are retried with exponential backoff.
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency is enforced using:
- Idempotency Keys: Each reservation request includes a unique
idempotencyKey(UUID or client-generated). - Cache-Based Deduplication: The system caches the result of each idempotency key for a short TTL (e.g., 5 minutes) to avoid reprocessing the same request.
- Database-Level Deduplication: The reservation table includes a unique constraint on
(idempotencyKey, itemSKU)to prevent duplicate entries.
public class InventoryReservationService {
private final Map<String, ReservationResult> idempotencyCache = new ConcurrentHashMap<>();
private final ScheduledExecutorService cacheCleanupScheduler = Executors.newScheduledThreadPool(1);
public InventoryReservationService() {
// Schedule cache cleanup every 5 minutes
cacheCleanupScheduler.scheduleAtFixedRate(() -> {
long now = System.currentTimeMillis();
idempotencyCache.entrySet().removeIf(entry -> now - entry.getValue().timestamp > 300000);
}, 5, 5, TimeUnit.MINUTES);
}
public ReservationResult reserveInventory(ReservationRequest request) {
String idempotencyKey = request.getIdempotencyKey();
ReservationResult cached = idempotencyCache.get(idempotencyKey);
if (cached != null) {
return cached;
}
// Proceed with reservation logic
ReservationResult result = performReservation(request);
idempotencyCache.put(idempotencyKey, new ReservationResult(result, System.currentTimeMillis()));
return result;
}
private ReservationResult performReservation(ReservationRequest request) {
// Retry loop with exponential backoff
int maxRetries = 3;
int delayMs = 100;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
return executeReservation(request);
} catch (InventoryConflictException e) {
if (attempt == maxRetries) throw e;
try {
Thread.sleep(delayMs * (1 << attempt)); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
}
}
throw new RuntimeException("Max retries exceeded");
}
private ReservationResult executeReservation(ReservationRequest request) {
// Transaction boundary with optimistic locking
return transactionTemplate.execute(status -> {
InventoryItem item = inventoryRepository.findBySkuForUpdate(request.getSku());
if (item.getAvailableQuantity() < request.getQuantity()) {
throw new InventoryConflictException("Insufficient inventory");
}
// Update with version check
int updatedRows = inventoryRepository.updateInventory(
request.getSku(),
item.getVersion(),
item.getAvailableQuantity() - request.getQuantity()
);
if (updatedRows == 0) {
throw new InventoryConflictException("Inventory updated by another process");
}
// Create reservation record
Reservation reservation = new Reservation();
reservation.setSku(request.getSku());
reservation.setQuantity(request.getQuantity());
reservation.setIdempotencyKey(request.getIdempotencyKey());
reservation.setReservationId(UUID.randomUUID().toString());
reservationRepository.save(reservation);
return new ReservationResult(true, "Reservation successful", reservation.getReservationId());
});
}
}
6. Sequence Diagrams for Key Flows
sequenceDiagram
participant C as Client (e.g. Order Service)
participant I as Inventory Service
participant DB as Database
participant E as Event Bus
C->>I: reserveItems(request)
I->>DB: select for update (items)
DB-->>I: locked items
activate I
I->>I: validate availability
I->>DB: update inventory (decrease stock)
DB-->>I: updated items
I->>DB: insert reservation record
DB-->>I: reservation ID
I->>E: publish inventoryReserved event
E-->>I: ack
I-->>C: reservationId, reservedItems
deactivate I
sequenceDiagram
participant C as Client (e.g. Order Service)
participant I as Inventory Service
participant DB as Database
participant E as Event Bus
C->>I: reserveItems(request)
I->>DB: select for update (items)
DB-->>I: locked items
activate I
I->>I: validate availability
I->>DB: update inventory (decrease stock)
DB-->>I: updated items
I->>DB: insert reservation record
DB-->>I: reservation ID
I->>E: publish inventoryReserved event
E-->>I: ack
I-->>C: reservationId, reservedItems
deactivate I
C->>I: cancelReservation(reservationId)
I->>DB: select reservation by ID
DB-->>I: reservation record
I->>DB: update inventory (restore stock)
DB-->>I: updated items
I->>DB: delete reservation record
DB-->>I: ack
I->>E: publish inventoryReleased event
E-->>I: ack
I-->>C: success
sequenceDiagram
participant C as Client (e.g. Order Service)
participant I as Inventory Service
participant DB as Database
participant E as Event Bus
C->>I: reserveItems(request)
I->>DB: select for update (items)
DB-->>I: locked items
activate I
I->>I: validate availability
I->>DB: update inventory (decrease stock)
DB-->>I: updated items
I->>DB: insert reservation record
DB-->>I: reservation ID
I->>E: publish inventoryReserved event
E-->>I: ack
I-->>C: reservationId, reservedItems
deactivate I
C->>I: confirmReservation(reservationId)
I->>DB: select reservation by ID
DB-->>I: reservation record
I->>DB: update reservation status to confirmed
DB-->>I: ack
I->>E: publish inventoryConfirmed event
E-->>I: ack
I-->>C: success
sequenceDiagram
participant C as Client (e.g. Order Service)
participant I as Inventory Service
participant DB as Database
participant E as Event Bus
C->>I: reserveItems(request)
I->>DB: select for update (items)
DB-->>I: locked items
activate I
I->>I: validate availability
I->>DB: update inventory (decrease stock)
DB-->>I: updated items
I->>DB: insert reservation record
DB-->>I: reservation ID
I->>E: publish inventoryReserved event
E-->>I: ack
I-->>C: reservationId, reservedItems
deactivate I
C->>I: cancelReservation(reservationId)
I->>DB: select reservation by ID
DB-->>I: reservation record
I->>DB: update inventory (restore stock)
DB-->>I: updated items
I->>DB: delete reservation record
DB-->>I: ack
I->>E: publish inventoryReleased event
E-->>I: ack
I-->>C: success
C->>I: confirmReservation(reservationId)
I->>DB: select reservation by ID
DB-->>I: reservation record
I->>DB: update reservation status to confirmed
DB-->>I: ack
I->>E: publish inventoryConfirmed event
E-->>I: ack
I-->>C: success
7. Caching Design
Cache Strategy Selection
For inventory reservation system, we'll use cache-aside pattern with write-through behavior for critical inventory data. This approach provides the best balance between consistency and performance for a reservation system where data accuracy is paramount.
Cache Key Design
Cache keys will follow a structured namespace pattern:
inventory:product:{productId}:reservation:{reservationId}
inventory:product:{productId}:stock:{stockId}
inventory:product:{productId}:availability
TTL Configuration
| Data Type | TTL | Reason |
|---|---|---|
| Product reservations | 5 minutes | Prevents stale reservations from blocking inventory |
| Product stock levels | 30 seconds | Frequent updates, short TTL to ensure freshness |
| Product availability | 1 minute | Balances performance with consistency |
| User session data | 30 minutes | Session-based access control |
Cache Eviction Policy
LRU (Least Recently Used) with TTL-based expiration as primary strategy. This ensures:
- Hot data stays in cache
- Memory pressure is managed automatically
- Data freshness is maintained through TTL
Cache Stampede Prevention
Implement probabilistic early expiry with 20% random variance:
// Pseudo-code for cache stampede prevention
public class CacheStampedePrevention {
private static final double STAMPED_EXPIRY_VARIANCE = 0.2;
public String getCachedValue(String key, Supplier<String> loader) {
String cached = cache.get(key);
if (cached != null) {
return cached;
}
// Acquire distributed lock before loading
if (distributedLock.acquire(key, 500)) {
try {
// Double-check pattern
String fresh = cache.get(key);
if (fresh != null) {
return fresh;
}
// Load and cache with randomized TTL
String value = loader.get();
int ttl = calculateRandomizedTTL(30, STAMPED_EXPIRY_VARIANCE);
cache.put(key, value, ttl);
return value;
} finally {
distributedLock.release(key);
}
}
// Fallback to cache if lock acquisition fails
return cache.get(key);
}
}
Distributed Cache Architecture
Using Redis Cluster with:
- 3 master nodes for data distribution
- 3 replica nodes per shard for high availability
- Redis Sentinel for automatic failover
- Cache warming during service startup
Cache Invalidation Strategy
Event-driven invalidation with cache tags:
- When inventory changes, publish
inventory.updatedevent - Consumers invalidate related cache entries by tag
- For immediate consistency, use write-through with cache invalidation on write operations
// Sample event-driven invalidation
@Component
public class InventoryCacheInvalidator {
@EventListener
public void handleInventoryUpdate(InventoryUpdatedEvent event) {
String cacheKey = "inventory:product:" + event.getProductId() + ":*";
redisTemplate.delete(cacheKey);
// Invalidate specific reservation if needed
if (event.getReservationId() != null) {
String reservationKey = "inventory:product:" + event.getProductId()
+ ":reservation:" + event.getReservationId();
redisTemplate.delete(reservationKey);
}
}
}
Cache Read-Through Pattern
@Service
public class InventoryService {
@Cacheable(value = "inventory", key = "#productId")
public ProductInventory getProductInventory(String productId) {
return inventoryRepository.findByProductId(productId);
}
@Cacheable(value = "reservations", key = "#reservationId")
public Reservation getReservation(String reservationId) {
return reservationRepository.findById(reservationId);
}
}
Cache Performance Considerations
- Cache hit ratio: Target > 90% for hot paths
- Cache miss penalty: Minimize by pre-warming and using async loading
- Memory allocation: 60% of cache memory for hot data, 40% for metadata and tags
- Cache warming: Load frequently accessed products during service startup
Cache Consistency Trade-offs
The cache strategy prioritizes eventual consistency over strict consistency for performance, with:
- Immediate cache invalidation on write operations
- TTL-based automatic cleanup to prevent stale data
- Distributed locking for critical update operations
- Cache warming for frequently accessed items
This approach provides optimal performance while maintaining acceptable consistency levels for inventory reservation use cases.
8. Repository / Data Access Layer
Repository Pattern Implementation
The repository pattern is implemented to abstract data access logic and provide a clean separation between domain logic and database operations. Each entity will have its own repository interface and implementation.
public interface InventoryItemRepository {
Optional<InventoryItem> findById(UUID id);
List<InventoryItem> findBySku(String sku);
List<InventoryItem> findAll();
InventoryItem save(InventoryItem item);
void delete(UUID id);
boolean existsById(UUID id);
}
public interface ReservationRepository {
Optional<Reservation> findById(UUID id);
List<Reservation> findByInventoryItemId(UUID inventoryItemId);
List<Reservation> findByStatus(ReservationStatus status);
List<Reservation> findActiveReservationsBySku(String sku, Instant now);
Reservation save(Reservation reservation);
void delete(UUID id);
}
ORM vs. Raw Query Decisions
For the inventory system, we'll use JPA/Hibernate for most operations due to its maturity and ability to handle complex relationships, but raw SQL will be used for performance-critical operations like bulk reservations and inventory adjustments.
// JPA entities with proper mapping
@Entity
@Table(name = "inventory_items")
public class InventoryItem {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(unique = true, nullable = false)
private String sku;
@Column(nullable = false)
private Integer availableQuantity;
@Column(nullable = false)
private Integer reservedQuantity;
@Column(name = "version")
@Version
private Long version;
}
// Raw SQL for bulk operations
@Query(value = "UPDATE inventory_items SET available_quantity = available_quantity - :quantity, reserved_quantity = reserved_quantity + :quantity WHERE sku = :sku AND available_quantity >= :quantity AND version = :version")
int updateInventoryForReservation(@Param("sku") String sku,
@Param("quantity") Integer quantity,
@Param("version") Long version);
Read vs. Write Separation
We'll implement CQRS pattern for better scalability and performance:
// Read model - optimized for queries
@Entity
@Table(name = "inventory_read_model")
public class InventoryReadModel {
@Id
private String sku;
@Column(name = "available_quantity")
private Integer availableQuantity;
@Column(name = "reserved_quantity")
private Integer reservedQuantity;
// No version field for read model - updates are batched
}
// Write model - for mutations
@Entity
@Table(name = "inventory_items")
public class InventoryItem {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(unique = true, nullable = false)
private String sku;
@Column(nullable = false)
private Integer availableQuantity;
@Column(nullable = false)
private Integer reservedQuantity;
@Column(name = "version")
@Version
private Long version;
}
Connection Pool Configuration
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
validation-timeout: 5000
leak-detection-threshold: 60000
N+1 Query Prevention Strategy
We'll use eager loading and batch fetching to prevent N+1 queries:
// Using JOIN FETCH to avoid N+1 in reservation retrieval
@Query("SELECT r FROM Reservation r JOIN FETCH r.inventoryItem WHERE r.status = :status")
List<Reservation> findReservationsWithInventoryItems(@Param("status") ReservationStatus status);
// Using @BatchSize for collections
@Entity
public class InventoryItem {
@Id
private UUID id;
@OneToMany(mappedBy = "inventoryItem", fetch = FetchType.LAZY)
@BatchSize(size = 20)
private List<Reservation> reservations;
}
Batch Operation Design
For high-volume operations like bulk inventory updates:
public class BatchInventoryUpdate {
public void updateMultipleItems(List<InventoryUpdateRequest> updates) {
String sql = "UPDATE inventory_items SET available_quantity = CASE sku WHEN ? THEN ? WHEN ? THEN ? END WHERE sku IN (?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
// Batch update logic here
for (int i = 0; i < updates.size(); i++) {
InventoryUpdateRequest update = updates.get(i);
stmt.setString(1 + (i * 2), update.getSku());
stmt.setInt(2 + (i * 2), update.getNewAvailableQuantity());
}
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to batch update inventory", e);
}
}
}
Transaction Management
@Service
@Transactional(readOnly = true)
public class InventoryQueryService {
@Autowired
private InventoryItemRepository inventoryItemRepository;
@Transactional
public void processReservation(ReservationRequest request) {
// Transaction boundary for reservation creation and inventory update
InventoryItem item = inventoryItemRepository.findBySku(request.getSku())
.orElseThrow(() -> new InventoryException("Item not found"));
// Business logic and persistence in same transaction
Reservation reservation = createReservation(item, request);
reservationRepository.save(reservation);
}
}
Repository Implementation Example
@Repository
public class JpaInventoryItemRepository implements InventoryItemRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Optional<InventoryItem> findById(UUID id) {
return Optional.ofNullable(entityManager.find(InventoryItem.class, id));
}
@Override
public List<InventoryItem> findBySku(String sku) {
return entityManager.createQuery(
"SELECT i FROM InventoryItem i WHERE i.sku = :sku",
InventoryItem.class)
.setParameter("sku", sku)
.getResultList();
}
@Override
public InventoryItem save(InventoryItem item) {
if (item.getId() == null) {
entityManager.persist(item);
} else {
entityManager.merge(item);
}
return item;
}
@Override
public boolean existsById(UUID id) {
return entityManager.find(InventoryItem.class, id) != null;
}
@Override
public void delete(UUID id) {
InventoryItem item = entityManager.find(InventoryItem.class, id);
if (item != null) {
entityManager.remove(item);
}
}
}
Read-Write Splitting Strategy
// Custom routing data source for read/write separation
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseTypeContextHolder.getDatabaseType();
}
}
// Usage in service layer
@Service
public class InventoryService {
@Transactional(readOnly = true)
public List<InventoryItem> getAvailableItems() {
// This will route to read replica
return inventoryItemRepository.findAll();
}
@Transactional
public Reservation reserveItems(ReservationRequest request) {
// This will route to write replica
return reservationService.createReservation(request);
}
}
Performance Considerations
- Query Optimization: Use database indexes on frequently queried fields like SKU and status
- Connection Pooling: Properly sized connection pool with appropriate timeouts
- Query Caching: Cache frequently accessed read models using Redis or in-memory cache
- Batch Operations: Batch updates for bulk operations to reduce database round trips
- Partitioning Strategy: Partition inventory items by SKU prefix for better query performance
The repository layer is designed to be highly testable with mock implementations and supports both transactional consistency and high-performance read operations through CQRS and read replicas.
9. Asynchronous Processing & Messaging
Event-driven boundaries in this feature
The inventory reservation system will use a hybrid synchronous/asynchronous approach. The core order placement flow will be synchronous to ensure immediate feedback to the user, while inventory reservation and related downstream operations like stock deduction and notification will be handled asynchronously to improve system responsiveness and scalability.
Key triggers for asynchronous processing:
- Reservation creation - When a user attempts to reserve inventory items
- Stock level changes - When inventory levels are updated via external systems or manual adjustments
- Reservation expiration - When reservations timeout and need cleanup
- Order confirmation - When an order is confirmed and stock needs to be committed
- Reservation cancellation - When reservations are explicitly cancelled
Message Schema Design
{
"version": "1.0",
"event_type": "inventory.reservation.created",
"timestamp": "2023-11-15T14:30:00Z",
"correlation_id": "order-12345",
"payload": {
"reservation_id": "res-uuid-12345",
"product_id": "prod-uuid-67890",
"quantity": 2,
"user_id": "user-uuid-54321",
"reservation_expires_at": "2023-11-15T14:45:00Z",
"status": "created"
}
}
Dead letter queue strategy and poison message handling
All inventory-related messages will be processed through a dead-letter queue (DLQ) system with the following strategy:
- Messages that fail processing after 3 retries will be moved to DLQ
- Poison messages in DLQ will trigger alerts and manual investigation
- DLQ messages will be periodically reviewed by operations team for reprocessing or cleanup
- Failed messages will include metadata about the original error and retry count
At-least-once vs. exactly-once delivery
At-least-once delivery is selected for this system because:
- Inventory reservation operations are idempotent (re-reserving same items is safe)
- The system can handle duplicate events gracefully through reservation state tracking
- Simpler implementation and better performance compared to exactly-once semantics
- Most downstream systems (like order processing) can handle duplicate events
Retry policy for consumers
Retry policy configuration:
- Maximum retries: 3 attempts
- Backoff strategy: Exponential with jitter (1s, 2s, 4s)
- Retryable errors: Network timeouts, database connection failures, transient service unavailability
- Non-retryable errors: Invalid payload format, authorization failures, business rule violations
public class RetryPolicy {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
public boolean shouldRetry(Exception exception) {
return exception instanceof NetworkException ||
exception instanceof DatabaseException ||
exception instanceof ServiceUnavailableException;
}
public long getDelay(int attempt) {
return (long) (BASE_DELAY_MS * Math.pow(2, attempt) +
ThreadLocalRandom.current().nextDouble() * 1000);
}
}
Outbox pattern for reliable event publishing
Outbox pattern is implemented to ensure eventual consistency between database writes and message publishing:
public class InventoryReservationService {
@Transactional
public Reservation createReservation(ReservationRequest request) {
// 1. Create reservation in database
Reservation reservation = reservationRepository.save(
new Reservation(request.getProductId(), request.getQuantity()));
// 2. Store event in outbox table
OutboxEvent outboxEvent = new OutboxEvent(
"inventory.reservation.created",
reservation.getId(),
objectMapper.writeValueAsString(reservation),
OutboxStatus.PENDING);
outboxRepository.save(outboxEvent);
// 3. Publish to message queue (handled by background processor)
return reservation;
}
}
Sequence diagram for async flows
sequenceDiagram
participant User
participant OrderService
participant ReservationService
participant InventoryRepository
participant OutboxRepository
participant MessageQueue
participant NotificationService
User->>OrderService: Place Order (with items)
OrderService->>ReservationService: Reserve inventory items
ReservationService->>InventoryRepository: Check and reserve stock
ReservationService->>OutboxRepository: Store reservation event
OutboxRepository->>MessageQueue: Publish to queue
MessageQueue->>NotificationService: Notify user of reservation
loop Retry on failure
NotificationService->>MessageQueue: Retry failed delivery
end
10. Error Handling Design
Error Classification Hierarchy
The system implements a hierarchical error classification to enable proper handling and alerting:
Error
├── ValidationException (400)
│ ├── InvalidInputException
│ └── ConstraintViolationException
├── BusinessLogicException (409)
│ ├── InsufficientStockException
│ ├── ReservationConflictException
│ └── InvalidReservationStateException
├── InfrastructureException (500)
│ ├── DatabaseException
│ ├── ExternalServiceException
│ └── CacheException
└── AuthorizationException (403)
Error Propagation Strategy
- Domain errors (InsufficientStockException, ReservationConflictException) are caught at service boundaries and converted to appropriate HTTP 409 responses
- Infrastructure errors (DatabaseException, ExternalServiceException) are wrapped and re-thrown to allow higher-level services to handle retries or fallbacks
- Validation errors (InvalidInputException) are immediately returned as HTTP 400 without logging to avoid noise
- Authorization errors (AuthorizationException) are returned as HTTP 403 with appropriate audit logging
Retry Strategy
For infrastructure-level errors, the system implements exponential backoff with jitter:
public class RetryableOperation {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
public <T> T executeWithRetry(Supplier<T> operation) {
int attempts = 0;
while (attempts < MAX_RETRIES) {
try {
return operation.get();
} catch (InfrastructureException e) {
if (attempts >= MAX_RETRIES - 1) throw e;
long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempts) +
ThreadLocalRandom.current().nextDouble() * 1000);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new InfrastructureException("Retry interrupted", ie);
}
attempts++;
}
}
throw new IllegalStateException("Unexpected execution path");
}
}
Circuit Breaker Design
For external service dependencies (e.g., payment gateway, notification service):
@Component
public class CircuitBreakerManager {
private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("inventory-service");
public <T> T withCircuitBreaker(Supplier<T> operation, Class<? extends Exception>... retryableExceptions) {
return circuitBreaker.executeSupplier(operation);
}
// Circuit breaker state transitions:
// CLOSED -> OPEN (after 5 consecutive failures within 10 seconds)
// OPEN -> HALF_OPEN (after 30 seconds)
// HALF_OPEN -> CLOSED (if next call succeeds)
// HALF_OPEN -> OPEN (if next call fails)
}
Fallback Behavior Design
public class InventoryService {
private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("external-service");
public ReservationResult reserveItems(List<ReservationRequest> requests) {
return circuitBreaker.executeSupplier(() -> {
try {
return processReservations(requests);
} catch (ExternalServiceException e) {
// Fallback to local cache or default behavior
return handleFallback(requests);
}
});
}
private ReservationResult handleFallback(List<ReservationRequest> requests) {
// Log fallback event for monitoring
logger.warn("Using fallback logic for reservation due to external service failure");
return new ReservationResult(ReservationStatus.FAILED, "Fallback applied");
}
}
Error Logging Format
All errors are logged with structured JSON format:
{
"timestamp": "2023-12-01T10:30:45.123Z",
"trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"span_id": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",
"user_id": "user-12345",
"error_code": "INSUFFICIENT_STOCK",
"error_message": "Requested quantity exceeds available stock for item SKU-12345",
"error_type": "BusinessLogicException",
"request_context": {
"reservation_id": "res-54321",
"items": [
{
"sku": "SKU-12345",
"requested_quantity": 10,
"available_quantity": 5
}
]
},
"stack_trace": "...",
"severity": "ERROR"
}
Alerting Thresholds
| Metric | Threshold | Alert Level | Escalation |
|---|---|---|---|
| Infrastructure errors (5min) | > 10 occurrences | Warning | On-call engineer |
| Business logic errors (5min) | > 5 occurrences | Warning | Team lead |
| Circuit breaker OPEN state | > 30 seconds | Critical | On-call engineer |
| Retry failures (5min) | > 20 occurrences | Warning | SRE team |
Error Response Format
All error responses follow a standardized JSON structure:
{
"error": {
"code": "INSUFFICIENT_STOCK",
"message": "Requested quantity exceeds available stock",
"details": {
"sku": "SKU-12345",
"requested": 10,
"available": 5
},
"timestamp": "2023-12-01T10:30:45.123Z"
}
}
Error Handling in Transaction Boundaries
@Transactional
public ReservationResult reserveItems(List<ReservationRequest> requests) {
try {
// Validate input
validateRequests(requests);
// Acquire locks on inventory items
acquireItemLocks(requests);
// Check stock availability
checkStockAvailability(requests);
// Create reservation records
List<Reservation> reservations = createReservations(requests);
// Update inventory levels
updateInventoryLevels(requests);
// Publish events
publishReservationEvents(reservations);
return new ReservationResult(ReservationStatus.CONFIRMED, "Success");
} catch (ValidationException e) {
// Log validation errors but don't alert
logger.warn("Validation error in reservation: {}", e.getMessage());
throw new ValidationException("Invalid reservation request", e);
} catch (BusinessLogicException e) {
// Log business errors and alert
logger.error("Business logic error in reservation: {}", e.getMessage(), e);
throw new BusinessLogicException(e.getCode(), e.getMessage(), e);
} catch (InfrastructureException e) {
// Log infrastructure errors and trigger alerting
logger.error("Infrastructure error in reservation: {}", e.getMessage(), e);
throw new InfrastructureException("Reservation operation failed", e);
}
}
11. Security Design
Input Validation and Sanitization Strategy
All API inputs are validated using a combination of:
- Jackson validation annotations for structured data (e.g.,
@NotNull,@Min,@Pattern) - Custom validation constraints for business logic (e.g., valid SKU format, quantity range)
- OWASP Java HTML Sanitizer for any text fields that may contain user-generated content
- Length limits on string inputs (max 255 chars for identifiers, 1000 for descriptions)
- Type coercion to prevent type confusion attacks
SQL Injection Prevention
- Prepared statements exclusively used for all database queries (no string concatenation)
- Parameterized queries for all dynamic inputs
- ORM framework (Hibernate) with strict mapping rules to prevent raw SQL injection
- Query whitelisting for dynamic query building (e.g., allowed sort fields, filterable columns)
Authentication Middleware Design
- JWT-based authentication with 1-hour expiration for API access tokens
- OAuth2 Authorization Code flow for third-party integrations
- API key authentication for service-to-service communication (with rotation policy)
- Middleware pipeline that validates tokens before any endpoint execution
- Token refresh mechanism with sliding window (renewal window: 30 minutes before expiry)
Authorization Enforcement
- Role-Based Access Control (RBAC) with roles:
ADMIN,INVENTORY_MANAGER,READ_ONLY - Resource-level permissions checked at service layer for sensitive operations:
INVENTORY_MANAGERcan create/update/delete inventory itemsREAD_ONLYcan only read inventory status
- Permission validation using a centralized policy engine (e.g., Spring Security with custom
@PreAuthorize) - Audit trail of all authorization decisions for security monitoring
Sensitive Data Encryption
- At rest: AES-256 encryption using AWS KMS for all sensitive fields (e.g., supplier contact info, pricing)
- In transit: TLS 1.3 with HSTS enabled for all API endpoints
- Database encryption using Transparent Data Encryption (TDE) on PostgreSQL
PII Handling and Masking Strategy
- PII fields (supplier emails, contact names) are encrypted at rest using AES-256
- Masking in logs: Email addresses shown as
user***@domain.com, phone numbers as+1 (555) ***-**** - Audit logs exclude PII fields unless explicitly required for compliance (e.g., fraud investigation)
- Data anonymization in development/test environments using deterministic masking
Secrets Management
- AWS Secrets Manager for storing:
- Database credentials (rotated every 90 days)
- Third-party API keys (e.g., shipping provider credentials)
- JWT signing keys (rotated every 6 months)
- Automated rotation using Lambda functions with KMS integration
- Least privilege access - only necessary services have read access to secrets
Audit Logging Design
- Event types tracked:
INVENTORY_ITEM_CREATED,INVENTORY_ITEM_UPDATED,RESERVATION_CREATEDACCESS_DENIED,UNAUTHORIZED_ACCESS_ATTEMPT
- Log format:
{
"timestamp": "2023-10-27T14:30:00Z",
"trace_id": "abc123def456",
"user_id": "user-123",
"operation": "RESERVE_INVENTORY",
"resource_id": "item-456",
"ip_address": "192.168.1.100",
"user_agent": "inventory-service/1.0",
"status": "SUCCESS",
"details": {
"sku": "SKU-12345",
"quantity": 10,
"reservation_id": "res-789"
}
}
- Retention policy: 90 days for audit logs, 1 year for compliance-relevant events
- Log aggregation using ELK stack with centralized alerting for suspicious patterns
12. Testing Strategy
Unit Testing Scope and Coverage Targets
- Target coverage: 85% code coverage for business logic classes and service methods
- Focus areas:
- Core reservation logic (success, failure, edge cases)
- Inventory validation and constraint checking
- Concurrency handling (race condition scenarios)
- Error path handling (insufficient stock, expired reservations)
- Event publishing and retry logic
- Testing frameworks: JUnit 5, Mockito, Testcontainers for integration components
Integration Testing Strategy
- Database integration: Using Testcontainers with PostgreSQL 13+ for real DB testing
- External service mocks: WireMock for third-party inventory system integrations
- Cross-service boundaries: Embedded Redis for caching layer testing, embedded Kafka for async messaging
- Test data setup: Factory pattern with fixtures to ensure test isolation and reproducibility
Contract Testing
- Framework: Pact JVM for consumer-driven contract testing between inventory service and dependent services
- Scope: API contracts for reservation creation, status updates, and cancellation endpoints
- Versioning strategy: Contract version alignment with service versioning to prevent breaking changes
Performance Testing Tool and Key Scenarios
- Tool: k6 (JavaScript-based load testing) for realistic concurrent reservation scenarios
- Key test scenarios:
- Concurrent reservations (1000+ simultaneous requests)
- Reservation expiration under load
- High-throughput stock updates with concurrent readers/writers
- Metrics tracked: p50/p95/p99 latency, throughput (req/sec), error rates, memory usage
Test Data Management Strategy
- Fixtures: YAML-based data fixtures for common test scenarios (product categories, stock levels)
- Factories: Java-based factory classes for generating realistic test data (e.g., random product IDs, varying quantities)
- Seeding: Database seeding scripts for initial test environment setup
- Isolation: Each test runs in isolated database transaction with rollback after completion to prevent cross-test contamination
13. Observability Design
Structured Logging Schema
All logs must follow a consistent schema with the following required fields:
{
"timestamp": "2023-10-15T14:30:45.123Z",
"level": "INFO|WARN|ERROR",
"service": "inventory-reservation-service",
"trace_id": "12345678-1234-1234-1234-123456789012",
"span_id": "abcdef12-3456-7890-abcd-ef1234567890",
"user_id": "user-12345",
"entity_id": "inventory-item-7890",
"operation": "reserveInventory",
"request_id": "req-abc123",
"message": "Inventory reservation successful",
"error_code": "INSUFFICIENT_STOCK",
"error_details": {
"stack_trace": "...",
"cause": "Not enough stock available"
},
"duration_ms": 150,
"metadata": {
"reservation_id": "res-xyz789",
"requested_quantity": 5,
"available_quantity": 2
}
}
PII masking rules:
- User IDs and session tokens are masked in logs
- Credit card numbers and other sensitive PII fields are redacted entirely
Distributed Tracing Instrumentation
Using OpenTelemetry with custom spans for key operations:
Span Types and Attributes:
-
Reservation Flow Span
- Name:
inventory.reservation.process - Attributes:
reservation_id: UUID of reservationitem_id: Inventory item identifierrequested_quantity: Quantity requested for reservationreservation_timeout_ms: Timeout duration in milliseconds
- Name:
-
Database Operation Spans
- Name:
db.inventory.update - Attributes:
db.operation: "UPDATE"db.statement: SQL query (sanitized)db.connection_id: Connection pool identifier
- Name:
-
External Service Call Spans
- Name:
external.service.inventory.check - Attributes:
service.name: "inventory-service"service.endpoint: "/api/inventory/check"
- Name:
Metrics Design
RED Method Metrics (Rate, Errors, Duration)
| Metric Name | Type | Labels | Description |
|---|---|---|---|
inventory_reservation_requests_total | Counter | status, operation | Total reservation requests |
inventory_reservation_errors_total | Counter | error_type, operation | Failed reservation operations |
inventory_reservation_duration_seconds | Histogram | operation, status | Reservation processing time |
inventory_reservation_active | Gauge | reservation_status | Currently active reservations |
inventory_stock_level | Gauge | item_id, location_id | Current stock levels |
Additional Service-Level Metrics:
| Metric Name | Type | Labels | Description |
|---|---|---|---|
db_connection_pool_available | Gauge | pool_name | Available database connections |
cache_hit_rate | Gauge | cache_type | Cache hit ratio for inventory items |
message_queue_processing_rate | Counter | queue_name, status | Async message processing rate |
retry_count_total | Counter | operation, retry_reason | Retry operation counts |
Health Check Endpoint Design
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/live")
public ResponseEntity<Map<String, Object>> live() {
Map<String, Object> response = new HashMap<>();
response.put("status", "LIVE");
response.put("timestamp", Instant.now());
return ResponseEntity.ok(response);
}
@GetMapping("/ready")
public ResponseEntity<Map<String, Object>> ready() {
Map<String, Object> response = new HashMap<>();
response.put("status", "READY");
response.put("timestamp", Instant.now());
response.put("database", databaseHealth());
response.put("cache", cacheHealth());
response.put("message_queue", messageQueueHealth());
// Return 503 if any component is unhealthy
if (isUnhealthy(response)) {
return ResponseEntity.status(503).body(response);
}
return ResponseEntity.ok(response);
}
private boolean isUnhealthy(Map<String, Object> health) {
return !health.get("database").equals("HEALTHY") ||
!health.get("cache").equals("HEALTHY") ||
!health.get("message_queue").equals("HEALTHY");
}
}
Alerting Thresholds
Critical Alerts (Escalation to On-Call):
| Metric | Threshold | Alert Condition |
|---|---|---|
inventory_reservation_errors_total | > 10 per minute | Error rate spike |
inventory_reservation_duration_seconds | > 5 seconds (p99) | Slow processing |
db_connection_pool_available | < 5 connections | Pool exhaustion |
cache_hit_rate | < 80% | Cache performance degradation |
Warning Alerts (Monitoring):
| Metric | Threshold | Alert Condition |
|---|---|---|
inventory_reservation_requests_total | > 1000 per minute | High traffic |
inventory_reservation_duration_seconds | > 2 seconds (p99) | Performance degradation |
message_queue_processing_rate | < 50 messages/sec | Queue processing slowdown |
On-Call Escalation Path:
- First Level: Service owners monitoring dashboards
- Second Level: On-call engineer (response within 5 minutes)
- Third Level: Platform team for infrastructure issues
- Fourth Level: Incident response team for service-wide outages
Service Monitoring Dashboard
Key dashboard panels:
- Reservation Throughput - Requests per second
- Error Rate - Error percentage over time
- Latency Distribution - p50/p95/p99 response times
- Database Health - Connection pool status, query performance
- Cache Performance - Hit rates, latency
- Message Queue Health - Message processing rate, queue depth
All metrics should be exported in Prometheus format for integration with monitoring systems like Grafana and Alertmanager.
14. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering alignment
- Reservation timeout strategy: Should reservations be automatically released after a fixed time (e.g., 15 minutes) or based on business logic (e.g., user activity, order completion)? This requires alignment with business requirements for inventory availability windows.
- Reservation scope granularity: Should reservations be at SKU level, product level, or warehouse level? The choice affects system complexity and consistency guarantees.
- Reservation expiration vs. cancellation policy: How should the system handle expired reservations that are not explicitly canceled? Should they be automatically released or require manual intervention?
Known trade-offs and justifications
-
Consistency vs. Availability Trade-off:
- High consistency (strong consistency) ensures all systems see the same inventory state but may reduce availability during network partitions or high load.
- Eventual consistency allows for better availability but introduces potential race conditions and requires more sophisticated conflict resolution.
- Decision: Use eventual consistency with optimistic locking for inventory updates, ensuring that the system remains available while maintaining strong consistency at the point of reservation.
-
Database vs. Cache Consistency:
- Cache-first approach: Faster reads but risk of stale data if cache is not properly invalidated.
- Database-first approach: Consistent but slower reads due to database round-trips.
- Decision: Use cache-aside pattern with TTL-based invalidation to balance performance and consistency.
-
Synchronous vs. Asynchronous Reservation:
- Synchronous reservation: Immediate feedback but can block user experience and increase latency.
- Asynchronous reservation: Better user experience but introduces eventual consistency and requires additional error handling.
- Decision: Use synchronous reservation for critical operations (e.g., during checkout) and asynchronous for non-critical inventory updates.
Risks and mitigation strategies
-
Race Condition Risk: Multiple concurrent requests for the same inventory item can lead to over-reservation.
- Mitigation: Implement optimistic locking with version numbers or database-level constraints to ensure atomic updates.
-
Cache Invalidation Risk: Stale cache entries can cause incorrect inventory availability.
- Mitigation: Implement TTL-based cache invalidation with event-driven invalidation for critical updates.
-
Deadlock Risk: Concurrent reservation operations can cause deadlocks in database transactions.
- Mitigation: Use consistent lock ordering and implement timeout strategies to prevent deadlocks.
-
Data Loss Risk: System failures during reservation process can lead to lost reservations or inventory inconsistencies.
- Mitigation: Implement transactional writes with proper rollback mechanisms and outbox pattern for event publishing.
Technical debt acknowledged
-
Monolithic Design Debt: The current design assumes a single inventory management system, which may not scale well for distributed systems or microservices architecture.
- Future Consideration: Plan for eventual migration to a distributed inventory system with proper event-driven architecture and cross-service coordination.
-
Hardcoded Reservation Timeouts: The reservation timeout is currently hardcoded, which limits flexibility for different business scenarios.
- Future Consideration: Make reservation timeout configurable via feature flags or configuration management to support different business requirements.
-
Limited Audit Trail Depth: Current audit logging is basic and may not capture all necessary details for compliance or debugging.
- Future Consideration: Enhance audit logging to include detailed context about reservation changes, user actions, and system states for better traceability.
-
No Cross-Service Coordination: The design assumes single-system operation, which could become problematic in a distributed environment where inventory might be shared across multiple services.
- Future Consideration: Implement proper saga or distributed transaction patterns to handle cross-service inventory reservations and consistency.