Skip to main content

Inventory Reservation System Design

Comprehensive low-level design for a scalable inventory reservation system with distributed locking and eventual consistency patterns.

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

  1. 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?
  2. What are the consistency requirements for inventory reservations?

    • Should we guarantee strong consistency (e.g., immediate availability updates) or can eventual consistency be acceptable?
  3. 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?
  4. What is the read/write ratio for inventory data?

    • Is the system primarily used for frequent inventory checks or more for placing reservations?
  5. How sensitive is the system to latency?

    • Are reservations expected to be processed in under 100ms for a good user experience?
  6. What authentication and authorization model is in place?

    • Are reservations made by authenticated users or via API keys?
  7. 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

MethodPathDescriptionAuth Required
POST/v1/inventory/reserveReserve inventory items for a specific orderYes
GET/v1/inventory/reserve/{reservationId}Get reservation details by IDYes
PUT/v1/inventory/reserve/{reservationId}Update reservation details (extend duration, change items)Yes
DELETE/v1/inventory/reserve/{reservationId}Release reservationYes
GET/v1/inventory/itemsList all inventory items with stock levelsYes
GET/v1/inventory/items/{itemId}Get specific inventory item detailsYes
POST/v1/inventory/itemsCreate new inventory itemAdmin Only
PUT/v1/inventory/items/{itemId}Update existing inventory itemAdmin 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 CodeDescription
200Success (GET, PUT)
201Created (POST)
400Bad Request - Invalid input
401Unauthorized - Missing or invalid auth
403Forbidden - Insufficient permissions
404Not Found - Resource doesn't exist
409Conflict - Reservation conflict or item already exists
422Unprocessable Entity - Validation errors
429Too Many Requests - Rate limit exceeded
500Internal 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):

  • customer role - can only access their own reservations
  • admin role - full CRUD access to inventory items and reservations
  • service role - 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 NameFieldsTypePurpose
idx_products_namenameStandardFast product lookups by name
idx_products_activeis_activeStandardEfficient filtering of active products
idx_inventory_items_productproduct_idStandardFast inventory lookups by product
idx_inventory_items_locationlocationStandardLocation-based inventory queries
idx_reservations_productproduct_idStandardFast reservation lookups by product
idx_reservations_inventory_iteminventory_item_idStandardFast reservation lookups by inventory item
idx_reservations_statusstatusStandardEfficient status-based filtering
idx_reservations_expires_atexpires_atStandardFast cleanup of expired reservations
idx_reservations_user_productuser_id, product_idCompositeFast 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 ExecutorService with a custom thread factory.
  • Async/Await Pattern: Asynchronous processing is implemented using CompletableFuture and 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:

  1. Optimistic Locking: Each inventory item includes a version field (e.g., version: int) that is validated during updates.
  2. Atomic Operations: Critical sections like inventory decrement are implemented using atomic operations or database-level atomic updates.
  3. 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:

  1. Lock Ordering: All inventory reservation operations acquire locks in a consistent order (e.g., by item ID or SKU).
  2. Timeout Strategy: Database transactions are configured with a timeout (e.g., 5 seconds) to prevent indefinite lock waits.
  3. Retry Logic: Failed transactions due to deadlocks are retried with exponential backoff.

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency is enforced using:

  1. Idempotency Keys: Each reservation request includes a unique idempotencyKey (UUID or client-generated).
  2. 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.
  3. 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 TypeTTLReason
Product reservations5 minutesPrevents stale reservations from blocking inventory
Product stock levels30 secondsFrequent updates, short TTL to ensure freshness
Product availability1 minuteBalances performance with consistency
User session data30 minutesSession-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:

  1. When inventory changes, publish inventory.updated event
  2. Consumers invalidate related cache entries by tag
  3. 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:

  1. Immediate cache invalidation on write operations
  2. TTL-based automatic cleanup to prevent stale data
  3. Distributed locking for critical update operations
  4. 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

  1. Query Optimization: Use database indexes on frequently queried fields like SKU and status
  2. Connection Pooling: Properly sized connection pool with appropriate timeouts
  3. Query Caching: Cache frequently accessed read models using Redis or in-memory cache
  4. Batch Operations: Batch updates for bulk operations to reduce database round trips
  5. 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

MetricThresholdAlert LevelEscalation
Infrastructure errors (5min)> 10 occurrencesWarningOn-call engineer
Business logic errors (5min)> 5 occurrencesWarningTeam lead
Circuit breaker OPEN state> 30 secondsCriticalOn-call engineer
Retry failures (5min)> 20 occurrencesWarningSRE 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_MANAGER can create/update/delete inventory items
    • READ_ONLY can 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_CREATED
    • ACCESS_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:

  1. Reservation Flow Span

    • Name: inventory.reservation.process
    • Attributes:
      • reservation_id: UUID of reservation
      • item_id: Inventory item identifier
      • requested_quantity: Quantity requested for reservation
      • reservation_timeout_ms: Timeout duration in milliseconds
  2. Database Operation Spans

    • Name: db.inventory.update
    • Attributes:
      • db.operation: "UPDATE"
      • db.statement: SQL query (sanitized)
      • db.connection_id: Connection pool identifier
  3. External Service Call Spans

    • Name: external.service.inventory.check
    • Attributes:
      • service.name: "inventory-service"
      • service.endpoint: "/api/inventory/check"

Metrics Design

RED Method Metrics (Rate, Errors, Duration)

Metric NameTypeLabelsDescription
inventory_reservation_requests_totalCounterstatus, operationTotal reservation requests
inventory_reservation_errors_totalCountererror_type, operationFailed reservation operations
inventory_reservation_duration_secondsHistogramoperation, statusReservation processing time
inventory_reservation_activeGaugereservation_statusCurrently active reservations
inventory_stock_levelGaugeitem_id, location_idCurrent stock levels

Additional Service-Level Metrics:

Metric NameTypeLabelsDescription
db_connection_pool_availableGaugepool_nameAvailable database connections
cache_hit_rateGaugecache_typeCache hit ratio for inventory items
message_queue_processing_rateCounterqueue_name, statusAsync message processing rate
retry_count_totalCounteroperation, retry_reasonRetry 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):

MetricThresholdAlert Condition
inventory_reservation_errors_total> 10 per minuteError rate spike
inventory_reservation_duration_seconds> 5 seconds (p99)Slow processing
db_connection_pool_available< 5 connectionsPool exhaustion
cache_hit_rate< 80%Cache performance degradation

Warning Alerts (Monitoring):

MetricThresholdAlert Condition
inventory_reservation_requests_total> 1000 per minuteHigh traffic
inventory_reservation_duration_seconds> 2 seconds (p99)Performance degradation
message_queue_processing_rate< 50 messages/secQueue processing slowdown

On-Call Escalation Path:

  1. First Level: Service owners monitoring dashboards
  2. Second Level: On-call engineer (response within 5 minutes)
  3. Third Level: Platform team for infrastructure issues
  4. Fourth Level: Incident response team for service-wide outages

Service Monitoring Dashboard

Key dashboard panels:

  1. Reservation Throughput - Requests per second
  2. Error Rate - Error percentage over time
  3. Latency Distribution - p50/p95/p99 response times
  4. Database Health - Connection pool status, query performance
  5. Cache Performance - Hit rates, latency
  6. 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.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    CQRS Framework Design and Implementation

    Comprehensive low-level design for a Command Query Responsibility Segregation (CQRS) framework with event sourcing, read/write model separation, and distributed consistency patterns.

  • Backend LLD

    Dynamic Configuration Service Low-Level Design

    Comprehensive LLD for a scalable dynamic configuration service with real-time updates, versioning, and multi-environment support.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.