Skip to main content

Pricing Engine Design and Implementation

Comprehensive low-level design for a scalable pricing engine system handling dynamic pricing rules, discounts, and real-time calculation.

A pricing engine is a critical backend component that dynamically calculates prices based on various business rules, discounts, and market conditions. This document outlines a robust architecture for handling complex pricing logic at scale.

1. Problem Statement & Requirements

Clarifying questions

  1. What is the scale of pricing rules and how many concurrent price calculations are expected?

    • This determines whether we need sharding, caching, or batch processing strategies.
  2. What consistency requirements exist for price calculations?

    • Do we need strong consistency (e.g., real-time price updates) or eventual consistency (e.g., for promotional pricing)?
  3. What are the key entities in the system?

    • Is it just products and pricing rules, or does it involve customers, locations, time zones, or dynamic discounts?
  4. What is the read/write ratio for pricing data?

    • If prices are frequently updated, we must design for write-heavy workloads with proper locking and caching.
  5. What latency targets are expected for price calculation?

    • For real-time e-commerce, sub-100ms p99 is likely required; for batch pricing engines, longer latencies may be acceptable.
  6. What authentication and authorization model is in place?

    • Will pricing rules be managed by admin users or via an external system (e.g., marketing automation)?
  7. Are there any external integrations or dependencies?

    • For example, does pricing depend on currency exchange rates, inventory levels, or third-party APIs?

Functional requirements

  • Support dynamic pricing rules — apply discounts, surcharges, or conditional logic based on criteria like customer segment, time window, or product category.
  • Calculate price for a given product — given a product ID and optional context (e.g., customer, location), return the final price.
  • Support rule precedence and conflict resolution — handle overlapping rules with clear priority logic.
  • Manage pricing rules — create, update, delete, and activate/deactivate pricing rules via an admin API.
  • Audit pricing rule changes — log all modifications to rules for compliance and debugging.
  • Support time-based pricing — apply rules only during specific time windows or recurring schedules.

Non-functional requirements

  • Latency targets:
    • p50: <50ms
    • p95: <100ms
    • p99: <200ms
  • Throughput: 10,000 price calculations/second (peak)
  • Availability: 99.9% uptime
  • Scalability: Support 10M+ products and 100K+ pricing rules

Constraints and assumptions

  • Assumption: Pricing engine is part of a larger commerce platform with existing product, customer, and order systems.
  • Assumption: Pricing rules are defined by business users or marketing teams via an admin UI.
  • Constraint: Rule evaluation must be deterministic and fast to support real-time price calculation.
  • Constraint: Pricing engine does not manage inventory or stock levels — these are handled by other systems.
  • Assumption: Pricing rules can be updated in real time without downtime.

Out of scope

  • Currency conversion logic — handled by a separate currency engine.
  • Inventory-based pricing (e.g., volume discounts) — handled by inventory or order systems.
  • Real-time price updates to frontend — handled by caching and CDN layers.
  • Multi-region pricing logic — handled by regional pricing services or configuration.
  • Pricing rule versioning and rollback — managed by external systems or audit logs.

2. API Design

API Style Selection

gRPC with Protocol Buffers is selected for the Pricing Engine API due to:

  • Strong typing and schema validation at compile time
  • Efficient binary serialization for high-throughput pricing calculations
  • Built-in support for streaming and bidirectional communication
  • Native support for versioning through proto package namespaces
  • Better performance characteristics than REST for complex pricing operations

Endpoint Definitions

MethodPathDescriptionAuthRate Limit
GetPrice/pricing.v1.PricingService/GetPriceCalculate price for a product with optional discounts and taxesJWT1000 req/hour
GetPriceRange/pricing.v1.PricingService/GetPriceRangeCalculate min/max price range for a product variantJWT500 req/hour
ApplyDiscounts/pricing.v1.PricingService/ApplyDiscountsApply multiple discounts to a priceJWT2000 req/hour
ValidatePriceRules/pricing.v1.PricingService/ValidatePriceRulesValidate price rule configurationsAdmin JWT100 req/hour
BulkCalculatePrices/pricing.v1.PricingService/BulkCalculatePricesCalculate prices for multiple products in batchJWT500 req/hour

Request and Response Schemas

// PricingService.proto

syntax = "proto3";

package pricing.v1;

message PriceRequest {
  string product_id = 1;
  string currency_code = 2;
  repeated string discount_codes = 3;
  map<string, string> context_attributes = 4;
  int32 quantity = 5;
  string customer_segment = 6;
}

message PriceResponse {
  double base_price = 1;
  double final_price = 2;
  repeated PriceComponent price_components = 3;
  string currency_code = 4;
  string price_id = 5;
  int64 calculated_at = 6;
}

message PriceComponent {
  string type = 1;
  double amount = 2;
  string description = 3;
  bool is_tax = 4;
  double tax_rate = 5;
}

message BulkPriceRequest {
  repeated PriceRequest requests = 1;
  string currency_code = 2;
}

message BulkPriceResponse {
  repeated PriceResponse responses = 1;
  repeated string failed_requests = 2;
}

message PriceRule {
  string rule_id = 1;
  string name = 2;
  string description = 3;
  PriceRuleCondition condition = 4;
  PriceRuleAction action = 5;
  int32 priority = 6;
  bool is_active = 7;
  int64 created_at = 8;
  int64 updated_at = 9;
}

message PriceRuleCondition {
  repeated string customer_segments = 1;
  repeated string product_categories = 2;
  map<string, string> attributes = 3;
  int32 min_quantity = 4;
}

message PriceRuleAction {
  enum ActionType {
    FIXED_DISCOUNT = 0;
    PERCENTAGE_DISCOUNT = 1;
    BULK_DISCOUNT = 2;
    PRICE_OVERRIDE = 3;
  }
  
  ActionType type = 1;
  double value = 2;
  string currency_code = 3;
}

message ValidatePriceRulesRequest {
  repeated PriceRule rules = 1;
}

message ValidatePriceRulesResponse {
  repeated ValidationIssue issues = 1;
  bool is_valid = 2;
}

message ValidationIssue {
  string field_path = 1;
  string message = 2;
  enum Severity {
    WARNING = 0;
    ERROR = 1;
  }
  Severity severity = 3;
}

HTTP Status Codes and Error Response Format

Status CodeDescription
200 OKSuccessful price calculation
400 Bad RequestInvalid request parameters
401 UnauthorizedMissing or invalid authentication
403 ForbiddenInsufficient permissions
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorServer-side error
503 Service UnavailableSystem temporarily unavailable

Standardized error response format:

{
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "Product ID is required",
    "details": [
      {
        "type": "field_violation",
        "field": "product_id",
        "description": "Product ID is required"
      }
    ]
  }
}

API Versioning Strategy

Versioned using gRPC package namespace:

  • pricing.v1 - Current stable version
  • pricing.v2 - Future breaking changes (e.g., new pricing types)
  • Versioning handled at service level, not endpoint level

Pagination Design

Cursor-based pagination is used for bulk operations:

  • Cursor contains last_processed_id and timestamp
  • Supports efficient large result set traversal
  • Handles concurrent data changes gracefully

Authentication and Authorization

Authentication: JWT Bearer tokens with custom claims:

  • user_id: Unique identifier
  • customer_segment: User's customer segment for pricing rules
  • roles: List of user roles (admin, member)

Authorization:

  • All endpoints require valid JWT with user_id
  • Admin endpoints (ValidatePriceRules) require admin role
  • Rate limiting per user ID and API key

Rate Limiting Strategy

Per-user rate limiting with sliding window:

  • 1000 requests/hour for regular users
  • 2000 requests/hour for premium users
  • 500 requests/hour for admin endpoints
  • Implementation uses Redis with atomic counters

Idempotency Design

Idempotency Key Strategy:

  • Clients send X-Idempotency-Key header (UUID)
  • Server stores operation result with key as cache key
  • Subsequent identical requests return cached result
  • TTL: 1 hour for price calculations, 24 hours for rule validation
// Example idempotency implementation in gRPC service
public class PricingService {
    private final RedisTemplate<String, String> redisTemplate;
    
    public PriceResponse getPrice(PriceRequest request, String idempotencyKey) {
        if (idempotencyKey != null) {
            String cachedResult = redisTemplate.opsForValue().get("idempotency:" + idempotencyKey);
            if (cachedResult != null) {
                return parseCachedResponse(cachedResult);
            }
        }
        
        PriceResponse response = calculatePrice(request);
        
        if (idempotencyKey != null) {
            String serialized = serializeResponse(response);
            redisTemplate.opsForValue().set(
                "idempotency:" + idempotencyKey, 
                serialized, 
                Duration.ofHours(1)
            );
        }
        
        return response;
    }
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    PRODUCT ||--o{ PRICE_TIER : "has"
    PRODUCT ||--o{ PROMOTION : "can have"
    PRICE_TIER ||--o{ PRICE_RULE : "contains"
    PROMOTION ||--o{ PROMOTION_RULE : "has"
    PRICE_TIER ||--o{ PRODUCT_PRICE : "applies to"
    PROMOTION ||--o{ PRODUCT_PROMOTION : "applies to"
    PRICE_RULE ||--o{ PROMOTION_RULE : "can be part of"
    
    PRODUCT {
        string id PK
        string name
        string category
        string sku
        timestamp created_at
        timestamp updated_at
    }
    
    PRICE_TIER {
        string id PK
        string name
        string description
        timestamp created_at
        timestamp updated_at
    }
    
    PRICE_RULE {
        string id PK
        string price_tier_id FK
        string rule_type
        string condition_json
        decimal discount_value
        timestamp created_at
        timestamp updated_at
    }
    
    PROMOTION {
        string id PK
        string name
        string description
        timestamp start_time
        timestamp end_time
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }
    
    PROMOTION_RULE {
        string id PK
        string promotion_id FK
        string rule_type
        string condition_json
        decimal discount_value
        timestamp created_at
        timestamp updated_at
    }
    
    PRODUCT_PRICE {
        string product_id FK
        string price_tier_id FK
        decimal amount
        timestamp effective_from
        timestamp effective_to
        timestamp created_at
        timestamp updated_at
    }
    
    PRODUCT_PROMOTION {
        string product_id FK
        string promotion_id FK
        timestamp created_at
        timestamp updated_at
    }

Table Schema Definitions

-- Products table
CREATE TABLE products (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    category VARCHAR(100),
    sku VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Price Tiers table
CREATE TABLE price_tiers (
    id UUID PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Price Rules table
CREATE TABLE price_rules (
    id UUID PRIMARY KEY,
    price_tier_id UUID REFERENCES price_tiers(id) ON DELETE CASCADE,
    rule_type VARCHAR(50) NOT NULL, -- e.g., "quantity", "customer_segment"
    condition_json JSONB NOT NULL,
    discount_value DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Promotions table
CREATE TABLE promotions (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    start_time TIMESTAMP NOT NULL,
    end_time TIMESTAMP NOT NULL,
    is_active BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Promotion Rules table
CREATE TABLE promotion_rules (
    id UUID PRIMARY KEY,
    promotion_id UUID REFERENCES promotions(id) ON DELETE CASCADE,
    rule_type VARCHAR(50) NOT NULL, -- e.g., "quantity", "customer_segment"
    condition_json JSONB NOT NULL,
    discount_value DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Product Prices table (mapping)
CREATE TABLE product_prices (
    product_id UUID REFERENCES products(id) ON DELETE CASCADE,
    price_tier_id UUID REFERENCES price_tiers(id) ON DELETE CASCADE,
    amount DECIMAL(10,2) NOT NULL,
    effective_from TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    effective_to TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (product_id, price_tier_id)
);

-- Product Promotions table (mapping)
CREATE TABLE product_promotions (
    product_id UUID REFERENCES products(id) ON DELETE CASCADE,
    promotion_id UUID REFERENCES promotions(id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (product_id, promotion_id)
);

Primary Key Strategy

UUIDs are used as primary keys for the following reasons:

  • Global uniqueness: UUIDs ensure uniqueness across distributed systems and databases
  • No coordination required: No need to coordinate with other services or databases for ID generation
  • Scalability: Support for horizontal scaling without ID conflicts
  • Security: Prevents enumeration attacks by making IDs unpredictable

Index Design

Index NameFieldsTypePurpose
idx_products_skuskuUniqueFast lookup by SKU
idx_price_tiers_namenameUniqueFast lookup by tier name
idx_price_rules_tier_idprice_tier_idStandardFast rule lookup by tier
idx_promotions_active_timeis_active, start_time, end_timePartialFast active promotion lookup
idx_product_prices_product_tierproduct_id, price_tier_idCompositeFast price lookup by product and tier
idx_product_prices_effectiveeffective_from, effective_toPartialFast price validity lookup
idx_promotion_rules_promotion_idpromotion_idStandardFast rule lookup by promotion

Data Partitioning / Sharding Strategy

Sharding by Product Category:

  • Partition products by category (e.g., electronics, clothing, books)
  • Each category gets its own database shard or table prefix
  • This provides natural separation of data and improves query performance

Alternative: Time-based sharding for price history if needed:

  • Partition product_prices by effective_from date ranges (monthly, quarterly)
  • Allows for efficient historical price queries and archiving

Soft Delete vs. Hard Delete Decision

Soft delete is used for the following reasons:

  • Audit trail: Historical price changes and promotions need to be preserved
  • Compliance: Regulatory requirements may mandate data retention
  • Recovery capability: Ability to restore deleted promotions or price rules
  • Data integrity: Avoid breaking references in analytics systems

Audit Trail Design

All tables include created_at and updated_at timestamps. For audit tracking:

-- Audit log table for sensitive operations
CREATE TABLE audit_logs (
    id UUID PRIMARY KEY,
    operation_type VARCHAR(50) NOT NULL, -- CREATE, UPDATE, DELETE
    table_name VARCHAR(100) NOT NULL,
    record_id UUID NOT NULL,
    user_id UUID, -- if available
    changes JSONB, -- previous and new values
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Data Archival Strategy

Price history archival:

  • Keep price history for 2 years in primary storage
  • Archive to cold storage (e.g., AWS Glacier) after 2 years
  • Use partitioning by effective_from date for easy archival

Promotion data:

  • Keep active promotions in hot storage
  • Archive completed promotions after 6 months
  • Store promotion metadata for analytics and reporting
-- Example archival policy trigger (pseudo-code)
CREATE FUNCTION archive_old_prices()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.effective_to < CURRENT_DATE - INTERVAL '2 years' THEN
        -- Move to archive table or cold storage
        INSERT INTO price_history_archive VALUES (OLD.*);
        DELETE FROM product_prices WHERE id = OLD.id;
    END IF;
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_archive_old_prices
    AFTER UPDATE ON product_prices
    FOR EACH ROW EXECUTE FUNCTION archive_old_prices();

4. Class Design & Service Layer

UML Class Diagram

classDiagram
    class PricingEngine {
        +calculatePrice(PriceRequest) PriceResponse
        +validateRules(PricingRule) boolean
    }
    
    class PriceRequest {
        +productId: String
        +quantity: int
        +customerTier: CustomerTier
        +promotionCodes: List~String~
        +currency: String
        +effectiveDate: LocalDateTime
    }
    
    class PriceResponse {
        +finalPrice: BigDecimal
        +basePrice: BigDecimal
        +discounts: List~Discount~
        +taxes: List~Tax~
        +currency: String
        +pricingRulesApplied: List~String~
    }
    
    class PricingRule {
        +id: UUID
        +ruleType: RuleType
        +priority: int
        +conditions: List~Condition~
        +actions: List~Action~
        +isActive: boolean
        +startDate: LocalDateTime
        +endDate: LocalDateTime
    }
    
    class Condition {
        +type: ConditionType
        +field: String
        +operator: Operator
        +value: Object
    }
    
    class Action {
        +type: ActionType
        +discountAmount: BigDecimal
        +discountPercentage: BigDecimal
        +taxRate: BigDecimal
    }
    
    class Discount {
        +amount: BigDecimal
        +percentage: BigDecimal
        +description: String
        +ruleId: UUID
    }
    
    class Tax {
        +rate: BigDecimal
        +type: String
        +description: String
    }
    
    class RuleEngine {
        +evaluateRules(PriceRequest, List~PricingRule~) List~Action~
    }
    
    class PricingContext {
        +request: PriceRequest
        +rules: List~PricingRule~
        +discounts: List~Discount~
        +taxes: List~Tax~
    }
    
    class PricingStrategy {
        +apply(PriceRequest, PricingContext) void
    }
    
    class TieredPricingStrategy {
        +apply(PriceRequest, PricingContext) void
    }
    
    class PromotionPricingStrategy {
        +apply(PriceRequest, PricingContext) void
    }
    
    class TaxCalculationStrategy {
        +calculate(PriceRequest, PricingContext) void
    }
    
    class PriceCalculator {
        +calculate(PriceRequest) PriceResponse
    }
    
    PricingEngine --> PriceCalculator : uses
    PriceCalculator --> RuleEngine : evaluates rules
    PriceCalculator --> PricingStrategy : applies strategies
    PricingStrategy <|-- TieredPricingStrategy : implements
    PricingStrategy <|-- PromotionPricingStrategy : implements
    RuleEngine --> PricingRule : processes rules
    
    class EventPublisher {
        +publish(PriceCalculatedEvent) void
    }
    
    PriceCalculator --> EventPublisher : publishes events
    
    class PriceRequestValidator {
        +validate(PriceRequest) ValidationResult
    }
    
    PricingEngine --> PriceRequestValidator : validates input
    
    enum RuleType {
        TIERED
        PROMOTION
        BULK_DISCOUNT
        CUSTOMER_TIER
    }
    
    enum ConditionType {
        PRODUCT_CATEGORY
        CUSTOMER_TIER
        QUANTITY
        DATE_RANGE
    }
    
    enum ActionType {
        DISCOUNT_PERCENTAGE
        DISCOUNT_AMOUNT
        TAX_RATE
        FIXED_PRICE
    }

State Machine Diagram for Pricing Rule

stateDiagram-v2
    [*] --> ACTIVE
    ACTIVE --> INACTIVE : Deactivate rule
    ACTIVE --> EXPIRED : End date reached
    INACTIVE --> ACTIVE : Reactivate rule
    EXPIRED --> [*]

Service Class Structure and Responsibilities

The Pricing Engine service is designed with a layered architecture:

  1. PricingEngine - Main entry point for price calculation
  2. PriceCalculator - Core business logic orchestrator
  3. RuleEngine - Rule evaluation and matching engine
  4. PricingStrategy - Strategy pattern implementations for different pricing models
  5. EventPublisher - Event publishing for price calculation events

Interface / Abstract Class Design

public interface PricingStrategy {
    void apply(PriceRequest request, PricingContext context);
}

public abstract class AbstractPricingStrategy implements PricingStrategy {
    protected final Logger logger = LoggerFactory.getLogger(getClass());
    
    @Override
    public void apply(PriceRequest request, PricingContext context) {
        if (shouldApply(request, context)) {
            execute(request, context);
        }
    }
    
    protected abstract boolean shouldApply(PriceRequest request, PricingContext context);
    protected abstract void execute(PriceRequest request, PricingContext context);
}

public interface RuleEngine {
    List<Action> evaluateRules(PriceRequest request, List<PricingRule> rules);
}

Business Logic Encapsulation Patterns

The Pricing Engine implements the Strategy Pattern for handling different pricing models:

public class TieredPricingStrategy extends AbstractPricingStrategy {
    
    @Override
    protected boolean shouldApply(PriceRequest request, PricingContext context) {
        return request.getCustomerTier() != null;
    }
    
    @Override
    protected void execute(PriceRequest request, PricingContext context) {
        // Apply tiered pricing logic
        BigDecimal basePrice = calculateBasePrice(request);
        BigDecimal tierDiscount = calculateTierDiscount(request, context);
        
        context.addDiscount(new Discount(tierDiscount, "Tiered discount", null));
    }
    
    private BigDecimal calculateBasePrice(PriceRequest request) {
        // Implementation logic here
        return BigDecimal.ZERO;
    }
    
    private BigDecimal calculateTierDiscount(PriceRequest request, PricingContext context) {
        // Implementation logic here
        return BigDecimal.ZERO;
    }
}

Critical-Path Method Implementation

public PriceResponse calculatePrice(PriceRequest request) {
    // Transaction boundary - all operations within single DB transaction
    return transactionTemplate.execute(status -> {
        try {
            // Validate input request
            ValidationResult validation = priceRequestValidator.validate(request);
            if (!validation.isValid()) {
                throw new PricingValidationException("Invalid price request", validation.getErrors());
            }
            
            // Load active pricing rules
            List<PricingRule> activeRules = pricingRuleRepository.findActiveRules(
                request.getEffectiveDate(), 
                request.getProductId()
            );
            
            // Create pricing context
            PricingContext context = new PricingContext(request, activeRules);
            
            // Apply pricing strategies in order of priority
            for (PricingStrategy strategy : pricingStrategies) {
                strategy.apply(request, context);
            }
            
            // Apply rule engine for complex conditions
            List<Action> actions = ruleEngine.evaluateRules(request, activeRules);
            applyActions(actions, context);
            
            // Calculate final price
            BigDecimal finalPrice = calculateFinalPrice(context);
            
            // Publish event for monitoring and audit
            eventPublisher.publish(new PriceCalculatedEvent(
                request.getProductId(),
                finalPrice,
                context.getDiscounts(),
                context.getTaxes()
            ));
            
            return new PriceResponse(
                finalPrice,
                context.getBasePrice(),
                context.getDiscounts(),
                context.getTaxes(),
                request.getCurrency(),
                context.getPricingRulesApplied()
            );
            
        } catch (Exception e) {
            // Log error and rethrow
            logger.error("Failed to calculate price for product: {}", request.getProductId(), e);
            throw new PricingCalculationException("Price calculation failed", e);
        }
    });
}

Transaction Boundary Design

The transaction boundary encompasses:

  1. Input validation
  2. Rule loading from database
  3. Pricing strategy application
  4. Action evaluation and application
  5. Final price calculation
  6. Event publishing

All operations within this boundary are wrapped in a single database transaction to ensure consistency.

Service Interface Contracts

public interface PricingService {
    PriceResponse calculatePrice(PriceRequest request) throws PricingCalculationException;
    
    List<PricingRule> getActiveRules(String productId, LocalDateTime effectiveDate);
    
    void createPricingRule(PricingRule rule) throws PricingValidationException;
    
    void updatePricingRule(String ruleId, PricingRule rule) throws PricingValidationException;
    
    void deletePricingRule(String ruleId) throws PricingValidationException;
}

Saga / Orchestration Pattern

For distributed transactions involving external systems (e.g., inventory or customer service), the Pricing Engine uses a Saga pattern:

public class PricingSaga {
    private final List<SagaStep> steps = new ArrayList<>();
    
    public void execute(SagaContext context) {
        for (SagaStep step : steps) {
            try {
                step.execute(context);
            } catch (Exception e) {
                // Compensate previous steps
                compensateSteps(context, step);
                throw new PricingSagaException("Saga failed", e);
            }
        }
    }
    
    private void compensateSteps(SagaContext context, SagaStep failedStep) {
        // Implementation for compensating previous steps
    }
}

Domain Object / Value Object Design

public class PriceRequest {
    private final String productId;
    private final int quantity;
    private final CustomerTier customerTier;
    private final List<String> promotionCodes;
    private final String currency;
    private final LocalDateTime effectiveDate;
    
    // Constructor, getters, equals, hashCode
}

public class PricingRule {
    private final UUID id;
    private final RuleType ruleType;
    private final int priority;
    private final List<Condition> conditions;
    private final List<Action> actions;
    private final boolean isActive;
    private final LocalDateTime startDate;
    private final LocalDateTime endDate;
    
    // Constructor, getters, equals, hashCode
}

public class Discount {
    private final BigDecimal amount;
    private final BigDecimal percentage;
    private final String description;
    private final UUID ruleId;
    
    // Constructor, getters, equals, hashCode
}

public class Tax {
    private final BigDecimal rate;
    private final String type;
    private final String description;
    
    // Constructor, getters, equals, hashCode
}

5. Concurrency & Thread Safety

Concurrency Model

The pricing engine will operate in a multi-threaded environment with the following concurrency model:

  • Thread pool for handling concurrent pricing requests (using Java's ExecutorService with fixed thread pool)
  • Async/Await pattern for I/O-bound operations (using CompletableFuture for non-blocking database calls and external service calls)
  • Reactive programming for high-throughput scenarios (using Project Reactor or RxJava for stream processing of pricing calculations)
  • Lock-free data structures where possible (ConcurrentHashMap, AtomicReference for state management)

Race Condition Prevention Strategy

Race conditions in pricing calculations will be mitigated through:

  1. Immutable pricing objects - all pricing entities are immutable to prevent concurrent modification
  2. Read-only operations - pricing calculations do not mutate state, only read from immutable sources
  3. Transaction isolation - database transactions ensure consistency during price updates
  4. Versioned pricing rules - optimistic locking on rule entities to prevent concurrent rule modifications

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for:

  • Pricing rule updates: Using version numbers in database to detect concurrent modifications
  • Price calculation cache updates: Using atomic compare-and-swap operations for cache entries

Pessimistic locking is used for:

  • Real-time price adjustments: When immediate consistency is required (e.g., during live sales)
  • Inventory-based pricing: When concurrent inventory updates can affect price calculation

Justification: Optimistic locking provides better performance and scalability for read-heavy pricing workloads, while pessimistic locking is reserved for critical real-time scenarios where immediate consistency is paramount.

Deadlock Prevention Strategy

Deadlock prevention mechanisms:

  1. Lock ordering: All pricing engine locks are acquired in a consistent order (e.g., by rule ID, then price ID)
  2. Timeout configuration: 5-second timeout for acquiring database locks to prevent indefinite blocking
  3. Lock hierarchy: Pricing rule locks are acquired before price calculation locks in all scenarios
  4. Deadlock detection: Database-level deadlock detection with automatic retry logic

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency is implemented using:

  1. Idempotency keys in pricing calculation requests
  2. Cache-based deduplication: Concurrent requests with same key are deduplicated using Redis or in-memory cache
  3. Database-level uniqueness constraints: Unique indexes on pricing request identifiers to prevent duplicate processing
public class PricingEngine {
    private final Map<String, CompletableFuture<PricingResult>> activeRequests = new ConcurrentHashMap<>();
    private final RedisTemplate<String, String> redisTemplate;
    
    public CompletableFuture<PricingResult> calculatePriceAsync(PricingRequest request) {
        String idempotencyKey = generateIdempotencyKey(request);
        
        // Check if request is already being processed
        return activeRequests.computeIfAbsent(idempotencyKey, key -> {
            CompletableFuture<PricingResult> future = CompletableFuture.supplyAsync(() -> {
                try {
                    // Check cache first
                    PricingResult cached = getCachedPrice(key);
                    if (cached != null) {
                        return cached;
                    }
                    
                    // Process pricing calculation
                    PricingResult result = processPricing(request);
                    
                    // Cache the result
                    cachePrice(key, result);
                    return result;
                } catch (Exception e) {
                    throw new PricingCalculationException("Failed to calculate price", e);
                }
            });
            
            // Remove from active requests after completion
            future.whenComplete((result, error) -> 
                activeRequests.remove(idempotencyKey));
            
            return future;
        });
    }
    
    private String generateIdempotencyKey(PricingRequest request) {
        return "pricing:" + 
            request.getProductId() + ":" + 
            request.getCustomerTier() + ":" + 
            request.getQuantity() + ":" + 
            request.getEffectiveDate().toInstant().toEpochMilli();
    }
}

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant C as Client
    participant API as PricingEngineAPI
    participant Service as PricingService
    participant Repo as PriceRepository
    participant Cache as RedisCache
    participant DB as PostgreSQLDB

    C->>API: GET /prices?product_id=123&quantity=5
    API->>Service: calculatePrice(product_id, quantity)
    Service->>Cache: get("price:123:5")
    Cache-->>Service: HIT
    Service-->>API: 200 OK {price: 150.00}
    API-->>C: 200 OK {price: 150.00}
sequenceDiagram
    participant C as Client
    participant API as PricingEngineAPI
    participant Service as PricingService
    participant Repo as PriceRepository
    participant Cache as RedisCache
    participant DB as PostgreSQLDB
    participant EventPublisher as EventPublisher

    C->>API: POST /prices/calculate
    API->>Service: calculatePrice(product_id, quantity, customer_segment)
    Service->>Cache: get("price:123:5:PREMIUM")
    Cache-->>Service: MISS
    Service->>Repo: findByProductAndSegment(product_id, segment)
    Repo-->>Service: PriceEntity{base_price=30.00, discount=0.1}
    Service->>Cache: set("price:123:5:PREMIUM", 27.00)
    Service->>EventPublisher: publishPriceCalculated(price_id, calculated_price)
    EventPublisher-->>Service: ack
    Service-->>API: 201 Created {price_id: "abc123", amount: 27.00}
    API-->>C: 201 Created {price_id: "abc123", amount: 27.00}
sequenceDiagram
    participant C as Client
    participant API as PricingEngineAPI
    participant Service as PricingService
    participant Repo as PriceRepository
    participant Cache as RedisCache
    participant DB as PostgreSQLDB
    participant EventPublisher as EventPublisher

    C->>API: POST /prices/calculate
    API->>Service: calculatePrice(product_id, quantity, customer_segment)
    Service->>Cache: get("price:123:5:PREMIUM")
    Cache-->>Service: MISS
    Service->>Repo: findByProductAndSegment(product_id, segment)
    Repo-->>Service: PriceEntity{base_price=30.00, discount=0.1}
    Service->>Cache: set("price:123:5:PREMIUM", 27.00)
    Service->>EventPublisher: publishPriceCalculated(price_id, calculated_price)
    EventPublisher-->>Service: ack
    Service->>Cache: set("price:123:5:PREMIUM", 27.00)
    Service-->>API: 201 Created {price_id: "abc123", amount: 27.00}
    API-->>C: 201 Created { price_id: "abc123", amount: 27.00 }

7. Caching Design

Cache Strategy Selection

For the pricing engine, we'll use a cache-aside pattern with LRU eviction policy. This approach provides the best balance between performance and consistency for a pricing system where prices can change based on time, inventory, or promotional rules.

Cache Key Design

Cache keys will follow a structured namespace pattern:

pricing:{entityType}:{entityId}:{contextHash}

Example key structure:

pricing:product:12345:7d8f9e1a2b3c4d5e6f7g8h9i0j1k2l3m

TTL Strategy

Data TypeTTLReason
Product Pricing5 minutesPrices change frequently, need to refresh regularly
Price Rules1 hourRules are less volatile, can be cached longer
Promotions15 minutesPromotional pricing is time-sensitive
User-specific Pricing30 secondsPersonalized prices may change per session

Cache Eviction Policy

We'll implement LRU (Least Recently Used) eviction with a time-based TTL. This ensures that:

  • Frequently accessed pricing data stays in cache
  • Old data is automatically evicted based on TTL
  • Memory pressure is managed through LRU eviction

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set TTL to 80% of actual TTL (e.g., 4 minutes for 5-minute cache)
  2. Distributed locks: Use Redis distributed locks when cache misses occur
  3. Cache warming: Pre-warm hot product pricing during low-traffic periods

Distributed Cache Architecture

We'll use Redis Cluster with:

  • 3 master nodes for write availability
  • 3 replica nodes for read availability
  • Auto-failover capability
  • Cross-region replication for disaster recovery

Cache Invalidation Strategy

Event-driven invalidation will be the primary strategy:

  1. When price rules change → invalidate all related pricing entries
  2. When promotions update → invalidate promotion-specific pricing
  3. When product inventory changes → invalidate affected price entries

TTL-based fallback: Cache entries will expire naturally if event-driven invalidation fails.

Cache Implementation Example

@Component
public class PricingCache {
    private final RedisTemplate<String, Object> redisTemplate;
    
    public Optional<PricingResult> getCachedPricing(String productId, PricingContext context) {
        String cacheKey = buildCacheKey(productId, context);
        
        // Try to get from cache
        Object cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return Optional.of((PricingResult) cached);
        }
        
        // Cache miss - acquire distributed lock to prevent stampede
        String lockKey = cacheKey + ":lock";
        if (redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", Duration.ofSeconds(5))) {
            try {
                // Double-check cache after lock acquisition
                cached = redisTemplate.opsForValue().get(cacheKey);
                if (cached != null) {
                    return Optional.of((PricingResult) cached);
                }
                return Optional.empty();
            } finally {
                // Release lock
                redisTemplate.delete(lockKey);
            }
        }
        
        // Another thread is computing, wait and retry
        return Optional.empty();
    }
    
    public void cachePricing(String productId, PricingContext context, PricingResult result) {
        String cacheKey = buildCacheKey(productId, context);
        redisTemplate.opsForValue().set(cacheKey, result, Duration.ofMinutes(5));
    }
    
    private String buildCacheKey(String productId, PricingContext context) {
        String contextHash = Hashing.murmur3_128()
            .hashString(context.toString(), StandardCharsets.UTF_8)
            .toString();
        return "pricing:product:" + productId + ":" + contextHash;
    }
}

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 PricingRuleRepository {
    Optional<PricingRule> findById(UUID id);
    List<PricingRule> findByProductAndConditions(Product product, Map<String, Object> conditions);
    void save(PricingRule rule);
    void delete(UUID id);
    List<PricingRule> findAll();
}

public class JdbcPricingRuleRepository implements PricingRuleRepository {
    private final DataSource dataSource;
    private final ObjectMapper objectMapper;

    @Override
    public Optional<PricingRule> findById(UUID id) {
        // Implementation using JDBC prepared statements
        String sql = "SELECT * FROM pricing_rules WHERE id = ?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setObject(1, id);
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                return Optional.of(mapResultSetToPricingRule(rs));
            }
        } catch (SQLException e) {
            throw new DataAccessException("Failed to fetch pricing rule", e);
        }
        return Optional.empty();
    }

    @Override
    public List<PricingRule> findByProductAndConditions(Product product, Map<String, Object> conditions) {
        // Implementation with dynamic query building
        String sql = buildQueryForConditions(product, conditions);
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            setParameters(stmt, conditions);
            ResultSet rs = stmt.executeQuery();
            return mapResultSetToPricingRules(rs);
        } catch (SQLException e) {
            throw new DataAccessException("Failed to find pricing rules", e);
        }
    }

    @Override
    public void save(PricingRule rule) {
        String sql = "INSERT INTO pricing_rules (id, product_id, conditions, price_calculation, created_at) VALUES (?, ?, ?, ?, ?)";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setObject(1, rule.getId());
            stmt.setObject(2, rule.getProduct().getId());
            stmt.setString(3, objectMapper.writeValueAsString(rule.getConditions()));
            stmt.setString(4, objectMapper.writeValueAsString(rule.getPriceCalculation()));
            stmt.setTimestamp(5, Timestamp.valueOf(rule.getCreatedAt()));
            stmt.executeUpdate();
        } catch (SQLException | JsonProcessingException e) {
            throw new DataAccessException("Failed to save pricing rule", e);
        }
    }

    @Override
    public void delete(UUID id) {
        String sql = "DELETE FROM pricing_rules WHERE id = ?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setObject(1, id);
            stmt.executeUpdate();
        } catch (SQLException e) {
            throw new DataAccessException("Failed to delete pricing rule", e);
        }
    }

    @Override
    public List<PricingRule> findAll() {
        String sql = "SELECT * FROM pricing_rules";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql);
             ResultSet rs = stmt.executeQuery()) {
            return mapResultSetToPricingRules(rs);
        } catch (SQLException e) {
            throw new DataAccessException("Failed to fetch all pricing rules", e);
        }
    }
}

ORM vs. Raw Query Decisions

For the pricing engine, raw SQL queries are preferred over ORM for performance-critical operations:

  1. Performance: Raw queries allow fine-tuning of database access patterns for complex pricing calculations
  2. Control: Direct control over query execution plans and index usage
  3. Complexity: Pricing rule matching involves complex joins and filtering that are hard to express with ORM

However, for simple CRUD operations (like creating new rules), we use JPA/Hibernate to reduce boilerplate code and ensure type safety.

Read vs. Write Separation

The pricing engine uses CQRS pattern for read-heavy operations:

public interface PricingRuleReadRepository {
    List<PricingRule> findActiveRulesByProduct(Product product);
    PricingRule findBestMatchingRule(Product product, Map<String, Object> context);
}

public class ReadModelPricingRuleRepository implements PricingRuleReadRepository {
    private final DataSource readDataSource;
    
    @Override
    public List<PricingRule> findActiveRulesByProduct(Product product) {
        // Optimized read query with proper indexes
        String sql = "SELECT * FROM pricing_rules WHERE product_id = ? AND active = true ORDER BY priority DESC";
        // Implementation details...
    }
    
    @Override
    public PricingRule findBestMatchingRule(Product product, Map<String, Object> context) {
        // Complex matching logic with optimized query plan
        String sql = "SELECT * FROM pricing_rules WHERE product_id = ? AND active = true";
        // Implementation details...
    }
}

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

To prevent N+1 queries when loading pricing rules with their associated products:

public class PricingRuleService {
    private final PricingRuleRepository ruleRepo;
    private final ProductRepository productRepo;

    public List<PricingRule> loadRulesWithProducts(List<UUID> ruleIds) {
        // Batch fetch all rules in single query
        List<PricingRule> rules = ruleRepo.findByIds(ruleIds);
        
        // Batch fetch all associated products in one query
        Set<UUID> productIds = rules.stream()
            .map(rule -> rule.getProduct().getId())
            .collect(Collectors.toSet());
        
        Map<UUID, Product> productMap = productRepo.findByIds(productIds)
            .stream()
            .collect(Collectors.toMap(Product::getId, Function.identity()));
            
        // Populate products in rules
        rules.forEach(rule -> {
            rule.setProduct(productMap.get(rule.getProduct().getId()));
        });
        
        return rules;
    }
}

Batch Operation Design

For bulk operations like rule imports:

public class PricingRuleBatchRepository {
    public void batchSave(List<PricingRule> rules) {
        String sql = "INSERT INTO pricing_rules (id, product_id, conditions, price_calculation, created_at) VALUES (?, ?, ?, ?, ?)";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            
            for (PricingRule rule : rules) {
                stmt.setObject(1, rule.getId());
                stmt.setObject(2, rule.getProduct().getId());
                stmt.setString(3, objectMapper.writeValueAsString(rule.getConditions()));
                stmt.setString(4, objectMapper.writeValueAsString(rule.getPriceCalculation()));
                stmt.setTimestamp(5, Timestamp.valueOf(rule.getCreatedAt()));
                stmt.addBatch();
            }
            
            stmt.executeBatch();
        } catch (SQLException | JsonProcessingException e) {
            throw new DataAccessException("Failed to batch save pricing rules", e);
        }
    }
}

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The pricing engine operates primarily in a synchronous request-response pattern for real-time price calculations, but certain operations benefit from asynchronous processing:

  1. Price calculation updates: When bulk pricing rules change or new pricing tiers are added, these should be processed asynchronously to avoid blocking user requests.
  2. Historical price archiving: Price history data older than 3 years should be archived to a cold storage system.
  3. Price change notifications: When prices change significantly, notify downstream systems (e.g., inventory, analytics) for cache invalidation or alerting.

The synchronous flows remain for immediate price lookups (e.g., real-time cart calculations) while asynchronous flows handle batch operations and long-running tasks.

Message schema design with sample payload

We use Protocol Buffers for message contracts to ensure type safety and versioning:

// PricingEvent.proto
syntax = "proto3";

package pricing;

message PriceChangeEvent {
  string price_id = 1;
  string product_id = 2;
  double old_price = 3;
  double new_price = 4;
  string currency = 5;
  int64 timestamp = 6;
  string event_source = 7;
  map<string, string> metadata = 8;
}

message PriceRuleUpdateEvent {
  string rule_id = 1;
  string product_category = 2;
  string rule_type = 3; // e.g., "tiered", "dynamic"
  string rule_definition = 4;
  int64 effective_at = 5;
  bool is_active = 6;
}

Versioning follows semantic versioning with major version bumps for breaking changes, minor versions for new features, and patch versions for bug fixes.

Dead letter queue strategy and poison message handling

For failed price calculation jobs, we implement a dead-letter queue (DLQ) strategy:

  1. Retry logic: Maximum 3 retries with exponential backoff (5s, 25s, 125s)
  2. DLQ: Messages that fail after max retries are moved to a pricing.dlq topic
  3. Manual review: DLQ messages are logged with full context and require manual intervention
  4. Alerting: SLI/SLO-based alerts for DLQ queue size and processing delays

At-least-once vs. exactly-once delivery

We choose at-least-once delivery for price change notifications because:

  • Price consistency is more important than avoiding duplicate events
  • The downstream systems can handle idempotent updates
  • The cost of reprocessing is minimal compared to missing a price update

Retry policy for consumers

Retry strategy:

  • Max retries: 3 attempts
  • Backoff formula: jitter = random(0, 1) * min(30s, base_delay * 2^attempt)
  • Base delay: 5 seconds
  • Non-retryable errors: Invalid price rule format, invalid product ID

Outbox pattern for reliable event publishing

We do implement the outbox pattern to ensure price change events are published atomically with database writes:

// OutboxEvent.java
public class OutboxEvent {
    private String id;
    private String eventType;
    private String payload;
    private String status; // PENDING, PROCESSED, FAILED
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

When a price is updated:

  1. Create OutboxEvent record in DB
  2. Publish to message broker
  3. Update status to PROCESSED only after successful publish

Sequence diagram for async flows

sequenceDiagram
    participant P as PricingService
    participant DB as Database
    participant MQ as MessageBroker
    participant A as AnalyticsService
    participant I as InventoryService

    P->>DB: Update price rule
    DB->>P: Acknowledge update
    P->>DB: Create OutboxEvent (PriceChangeEvent)
    DB->>P: Acknowledge outbox creation
    P->>MQ: Publish PriceChangeEvent
    MQ->>A: Deliver to AnalyticsService
    MQ->>I: Deliver to InventoryService
    A->>MQ: Acknowledge receipt
    I->>MQ: Acknowledge receipt

This ensures that price change events are reliably delivered to downstream consumers while maintaining consistency in the pricing engine's internal state.

10. Error Handling Design

Error Classification Hierarchy

The pricing engine follows a structured error classification system:

Error
├── ValidationException (400)
│   ├── InvalidInputException
│   └── MissingFieldException
├── BusinessLogicException (400)
│   ├── InsufficientInventoryException
│   ├── InvalidPricingRuleException
│   └── PricingCalculationException
├── InfrastructureException (500)
│   ├── DatabaseException
│   ├── ExternalServiceException
│   └── CacheException
└── SystemException (500)
    ├── InternalServerException
    └── TimeoutException

Error Propagation Strategy

  1. Domain-level errors: Caught and wrapped in appropriate business exceptions before being propagated up to the API layer
  2. Infrastructure errors: Bubble up as-is to allow for circuit breaker and retry logic at higher levels
  3. Validation errors: Caught early in the service layer and converted to standardized validation responses

Retry Strategy with Exponential Backoff and Jitter

public class RetryableOperation {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 1000;
    private static final double JITTER_FACTOR = 0.2;

    public <T> T executeWithRetry(Supplier<T> operation) {
        int attempt = 0;
        while (true) {
            try {
                return operation.get();
            } catch (ExternalServiceException e) {
                if (++attempt >= MAX_RETRIES) {
                    throw new RetryExhaustedException("Max retries exceeded", e);
                }
                
                long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempt - 1));
                delay = (long) (delay * (1 + JITTER_FACTOR * (Math.random() - 0.5)));
                
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RetryExhaustedException("Retry interrupted", ie);
                }
            }
        }
    }
}

Circuit Breaker Design

public class PricingCircuitBreaker {
    private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("pricing-service");
    
    public <T> T executeWithCircuitBreaker(Supplier<T> operation) {
        return circuitBreaker.executeSupplier(operation);
    }
    
    // Custom configuration for pricing service
    public static CircuitBreakerConfig customConfig() {
        return CircuitBreakerConfig.custom()
            .failureThreshold(5)
            .waitDurationInOpenState(Duration.ofSeconds(30))
            .slidingWindowSize(100)
            .permittedNumberOfCallsInHalfOpenState(5)
            .automaticTransitionFromOpenToHalfOpenEnabled(true)
            .build();
    }
}

Fallback Behavior Design

public class PricingFallbackHandler {
    
    public Price calculatePriceWithFallback(PricingRequest request) {
        // Fallback to default pricing rules when external services fail
        return new Price(
            request.getBasePrice(),
            Collections.singletonList(new PriceComponent("default", "fallback", 0.0)),
            "fallback"
        );
    }
    
    public List<PricingRule> getRulesWithFallback(String ruleSetId) {
        // Return cached or default rules when rule engine is unavailable
        return DEFAULT_RULES;
    }
}

Error Logging Format

All error logs follow a structured format:

{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "span_id": "f0e9d8c7-b6a5-4321-0fed-cba987654321",
  "user_id": "user-12345",
  "request_id": "req-67890",
  "error_code": "PRICING_CALCULATION_ERROR",
  "error_message": "Failed to calculate price for item: invalid currency code",
  "error_type": "BusinessLogicException",
  "component": "PricingService",
  "stack_trace": "...",
  "http_status": 400,
  "severity": "ERROR"
}

Alerting Thresholds

MetricWarning LevelCritical LevelAlerting Channel
Pricing calculation errors> 10 per minute> 50 per minuteOn-call
External service timeouts> 5 per minute> 20 per minuteSlack
Circuit breaker trips> 1 per hour> 5 per hourPagerDuty
Cache misses> 100 per minute> 500 per minuteEmail

Error Response Format

Standardized JSON error response:

{
  "error": {
    "code": "PRICING_RULE_INVALID",
    "message": "Invalid pricing rule configuration",
    "details": {
      "field": "discountPercentage",
      "value": "-5.0"
    },
    "timestamp": "2023-10-15T14:30:45.123Z"
  }
}

Error Handling in Service Layer

@Service
public class PricingService {
    
    public Price calculatePrice(PricingRequest request) {
        try {
            // Validate input
            validateRequest(request);
            
            // Fetch rules
            List<PricingRule> rules = pricingRuleService.getRules(request.getRuleSetId());
            
            // Calculate price
            Price calculatedPrice = priceCalculator.calculate(rules, request);
            
            return calculatedPrice;
            
        } catch (ValidationException e) {
            log.error("Validation error in pricing calculation", e);
            throw new PricingValidationException(e.getMessage(), e);
        } catch (ExternalServiceException e) {
            log.error("External service failure during pricing calculation", e);
            throw new PricingInfrastructureException("Pricing engine unavailable", e);
        } catch (Exception e) {
            log.error("Unexpected error in pricing calculation", e);
            throw new PricingSystemException("Internal pricing engine error", e);
        }
    }
}

11. Security Design

Input Validation and Sanitization Strategy

All API inputs are validated using a combination of:

  • Custom validation annotations (e.g., @Min(0), @NotNull) for primitive types
  • Regular expressions for string formats (e.g., currency codes, product IDs)
  • Domain-specific validators for complex types like pricing rules
  • OWASP Java HTML Sanitization library for any user-provided content that might be rendered

SQL Injection Prevention

  • Prepared statements are used exclusively for all database queries
  • Parameterized queries with no string concatenation in SQL generation
  • ORM framework (Hibernate) is used to abstract raw SQL, with strict mapping rules
  • Query whitelisting for dynamic query building (e.g., allowed sort fields, filterable attributes)

Authentication Middleware Design

  • JWT-based authentication with 2-hour expiration and 15-minute refresh window
  • OAuth2 integration for third-party service authentication (e.g., external price feeds)
  • API key validation for partner integrations with rate limiting
  • Authentication middleware that intercepts all requests and validates tokens or keys before proceeding to service layer

Authorization Enforcement

  • Role-Based Access Control (RBAC) with roles: ADMIN, PRICING_EDITOR, READ_ONLY
  • Permission checking at service layer using a decorator pattern:
@RequiresPermission("pricing:edit")
public void updatePricingRule(PricingRule rule) {
    // Implementation
}
  • Resource-level authorization for price rules (e.g., only users with access to specific product categories can modify their prices)
  • ABAC (Attribute-Based Access Control) for complex policies like "users from region X can only modify prices in their region"

Sensitive Data Encryption

  • At rest: AES-256 encryption using AWS KMS for all sensitive pricing data (e.g., cost prices, margin rules)
  • In transit: TLS 1.3 with HSTS enabled for all API endpoints
  • Key rotation: Automated key rotation every 90 days with seamless migration

PII Handling and Masking Strategy

  • No PII stored in pricing data tables (customer ID, personal details are not part of pricing records)
  • PII masking in logs and audit trails using regex patterns to mask sensitive identifiers
  • Audit trail only logs minimal user context (user ID, role) without personal details

Secrets Management

  • AWS Secrets Manager for storing API keys, database credentials, and encryption keys
  • Rotation policy: Secrets rotated every 90 days with automatic failover
  • Access control using IAM policies to restrict secret access to specific services only

Audit Logging Design

  • Event types logged: PRICE_RULE_CREATED, PRICE_RULE_UPDATED, PRICE_CALCULATION_REQUESTED
  • Log format: JSON with structured fields including:
{
  "timestamp": "2023-10-15T14:30:00Z",
  "user_id": "user-123",
  "role": "PRICING_EDITOR",
  "action": "PRICE_RULE_CREATED",
  "resource_id": "rule-456",
  "details": {
    "old_value": null,
    "new_value": {"product_id": "prod-123", "price": 100.0}
  }
}
  • Retention policy: 90 days for audit logs, archived to S3 with lifecycle policies
  • Log encryption: All logs encrypted at rest using AWS KMS

Additional Security Controls

  • Rate limiting per user and API key to prevent abuse (1000 req/hour for standard users, 5000 req/hour for partners)
  • Cross-site scripting (XSS) protection through content security policy headers and input sanitization
  • Security headers in HTTP responses (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
  • Input size limits to prevent buffer overflow attacks (max 10KB per request body)
  • Security scanning: Regular static analysis (SonarQube) and dynamic application security testing (DAST) integration in CI/CD pipeline

12. Testing Strategy

Unit Testing Scope and Coverage Targets (%)

  • Service Layer: 85% code coverage with focus on business logic, edge cases, and error paths
  • Domain Objects: 90% coverage including validation logic and state transitions
  • Repository Layer: 75% coverage with emphasis on query correctness and boundary conditions
  • Event Handlers: 80% coverage for async processing logic and error recovery paths
  • Core Pricing Calculation: 95% coverage with comprehensive test data sets including boundary values and edge cases

Integration Testing Strategy

  • Database Integration: Use testcontainers with PostgreSQL 14+ for end-to-end database interactions
  • External Service Mocking: WireMock for third-party rate provider integrations and payment gateway mocks
  • Event Broker Testing: Embedded Kafka or RabbitMQ for async flow validation and message processing
  • Caching Layer: Redis test container for cache interaction testing including TTL and eviction behavior
  • Authentication Integration: Mock OAuth2 server to validate JWT token processing and role-based access control

Contract Testing

  • API Contract Testing: Implement Pact for consumer-driven contract testing between pricing engine and dependent services (e.g., booking service, inventory service)
  • Event Schema Contract Testing: Use Avro schema registry with contract testing to ensure event payload consistency across services
  • External API Contracts: Define and test against documented external provider APIs (e.g., currency conversion, tax calculation services)

Performance Testing Tool and Key Scenarios

  • Load Testing: Use k6 for load testing with 1000 concurrent requests to pricing calculation endpoints
  • Stress Testing: Simulate 5000+ concurrent users to identify bottlenecks in pricing engine throughput
  • Key Scenarios:
    • Concurrent price calculation for same product with different parameters (1000 concurrent requests)
    • Batch price calculation for 1000 products with varying discounts and taxes
    • Price calculation under high cache miss conditions (50% cache hit rate)
    • Pricing engine with external service latency spikes (100ms to 500ms response times)
  • Metrics Tracked:
    • p95 latency for price calculation endpoint (target < 100ms)
    • Throughput (target > 5000 requests/sec)
    • Error rate (target < 0.1%)
    • Memory usage under load (target < 500MB)

Test Data Management Strategy

  • Fixtures: Use YAML-based fixtures for common pricing rules, product configurations, and currency data
  • Factories: Implement Factory pattern for generating test pricing data with various combinations of discounts, taxes, and time-based rules
  • Seeding: Database seeding scripts for test environment initialization with realistic pricing data sets
  • Data Isolation: Use database transactional isolation in tests to ensure clean test state and prevent cross-test contamination
  • Parameterized Testing: Use JUnit parameterized tests for boundary value analysis on price calculation logic (e.g., zero discounts, maximum taxes, negative values)

13. Observability Design

Structured Logging Schema

All logs follow a consistent JSON schema with the following required fields:

{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "level": "INFO|WARN|ERROR",
  "service": "pricing-engine",
  "trace_id": "1234567890abcdef1234567890abcdef",
  "span_id": "abcdef1234567890abcdef1234567890",
  "user_id": "user-123",
  "entity_id": "product-456",
  "request_id": "req-789",
  "operation": "calculatePrice",
  "message": "Price calculation completed successfully",
  "duration_ms": 45,
  "error_code": "VALIDATION_ERROR",
  "error_details": {
    "field": "quantity",
    "message": "Quantity must be positive"
  },
  "context": {
    "product_id": "prod-456",
    "currency": "USD",
    "quantity": 2
  }
}

PII masking rules:

  • User IDs are masked in logs (e.g., user-123 → user-***)
  • Credit card numbers and sensitive fields are redacted
  • Email addresses are masked (e.g., john.doe@example.com → j***.d***@example.com)

Distributed Tracing Instrumentation

Using OpenTelemetry with custom spans:

Key Operations to Trace:

  1. calculatePrice - Main pricing calculation operation
  2. applyDiscounts - Discount application logic
  3. fetchProduct - Product data retrieval
  4. validatePricingRules - Rule validation process

Span Attributes:

// Example span attributes for calculatePrice operation
span.setAttribute("pricing.product_id", productId);
span.setAttribute("pricing.currency", currency);
span.setAttribute("pricing.quantity", quantity);
span.setAttribute("pricing.customer_tier", customerTier);
span.setAttribute("pricing.rule_count", ruleCount);

Custom Spans:

  • PricingEngine.calculatePrice - Root operation span
  • PricingEngine.fetchProductData - Database query span
  • PricingEngine.applyRuleSet - Rule processing sub-span
  • PricingEngine.publishPriceEvent - Event publishing span

Metrics Design

RED Method Metrics:

Metric NameTypeLabelsDescription
pricing_calculations_totalCounteroperation, status, error_codeTotal pricing calculations
pricing_calculation_duration_secondsHistogramoperation, statusLatency distribution
pricing_errors_totalCounteroperation, error_type, error_codeError counts by type
pricing_cache_hits_totalCountercache_type, hit_statusCache hit/miss statistics
pricing_rules_loaded_totalGaugerule_type, versionActive pricing rules count

Label Design:

  • operation: calculatePrice, fetchProduct, applyDiscounts
  • status: success, failure, timeout
  • error_type: validation, internal, external
  • error_code: VALIDATION_ERROR, RULE_NOT_FOUND, DB_TIMEOUT
  • cache_type: product_cache, rule_cache, price_cache

Health Check Endpoint Design

@RestController
public class HealthController {
    
    @GetMapping("/health")
    public ResponseEntity<HealthStatus> health() {
        HealthStatus status = new HealthStatus();
        status.setTimestamp(System.currentTimeMillis());
        status.setStatus(HealthStatus.Status.HEALTHY);
        
        // Check database connectivity
        boolean dbHealthy = databaseService.isHealthy();
        status.setDatabase(dbHealthy ? HealthStatus.Status.HEALTHY : HealthStatus.Status.UNHEALTHY);
        
        // Check cache connectivity
        boolean cacheHealthy = cacheService.isHealthy();
        status.setCache(cacheHealthy ? HealthStatus.Status.HEALTHY : HealthStatus.Status.UNHEALTHY);
        
        // Check external service connectivity
        boolean externalHealthy = externalService.isHealthy();
        status.setExternalServices(externalHealthy ? HealthStatus.Status.HEALTHY : HealthStatus.Status.UNHEALTHY);
        
        return ResponseEntity.ok(status);
    }
    
    @GetMapping("/ready")
    public ResponseEntity<String> ready() {
        // Check if service is ready to serve requests
        return ResponseEntity.ok("Service is ready");
    }
    
    @GetMapping("/live")
    public ResponseEntity<String> live() {
        // Basic liveness check
        return ResponseEntity.ok("Service is alive");
    }
}

Alerting Thresholds

Critical Alerts (Escalation to On-call):

  • pricing_calculation_duration_seconds p99 > 500ms (warn: 200ms, critical: 500ms)
  • pricing_errors_total rate > 10/minute (warn: 5/minute, critical: 10/minute)
  • pricing_cache_hits_total ratio < 80% (warn: 85%, critical: 80%)

Warning Alerts:

  • pricing_calculation_duration_seconds p95 > 200ms (warn: 100ms, critical: 200ms)
  • pricing_errors_total rate > 2/minute (warn: 1/minute, critical: 2/minute)
  • pricing_cache_hits_total ratio < 90% (warn: 95%, critical: 90%)

On-call Escalation Path:

  1. First alert → Slack notification to on-call engineer
  2. Second alert within 5 minutes → Phone call to primary on-call
  3. Third alert within 10 minutes → Escalate to engineering manager

Service-Level Observability

Key Metrics Dashboard:

  1. Pricing calculation success rate (99.9% target)
  2. Latency distribution (p50/p95/p99 < 200ms)
  3. Cache hit ratio (target > 95%)
  4. Error rate (target < 0.1%)
  5. Rule loading success rate (target > 99.9%)

Service Dependencies:

  • Database read/write latency
  • External API response times (e.g., currency conversion)
  • Cache service availability
  • Event bus connectivity

Error Budget Monitoring:

  • Error budget burn rate per operation
  • Service-level error budget allocation (e.g., 1% for pricing calculations)
  • Alerting when error budget is consumed at >80% rate

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Pricing rule complexity vs. performance trade-off: Should we support complex rule chains with conditional logic (e.g., "if customer is VIP and purchase > $1000, apply 20% discount") or simplify to basic tiered pricing? Complex rules require more computational overhead and can impact real-time pricing calculation latency.
  • Real-time vs. batch pricing updates: Should pricing engine recalculate all prices in real-time on every request, or pre-calculate and cache results with periodic batch updates? Real-time provides accuracy but increases latency; batch improves performance but introduces staleness.
  • Currency support strategy: Will we support multiple currencies in real-time or pre-calculate for specific currency pairs? Real-time multi-currency calculation increases complexity and requires external exchange rate services.
  • Pricing tiers vs. dynamic pricing: Should we support fixed tiered pricing (e.g., 1-10 items = $10, 11-20 items = $8) or truly dynamic pricing that adapts to market conditions, inventory levels, and customer behavior?

Known trade-offs and justifications

Trade-offOption 1Option 2Justification
Consistency vs. AvailabilityStrong consistency with distributed transactionsEventual consistency with eventual price updatesFor a pricing engine, customers expect immediate accurate pricing for their current session. Strong consistency ensures correctness but may impact availability during high load. Eventual consistency allows better availability but risks showing outdated prices.
Complexity vs. PerformanceComplex rule engine with extensive conditionalsSimple tiered pricing rulesComplex rule engine provides flexibility but increases computational overhead and latency. Tiered pricing is simpler to implement and maintain, with predictable performance characteristics.
Cache freshness vs. cache hit ratioShort TTLs (30 seconds) for high accuracyLong TTLs (5 minutes) for better cache hit ratioShort TTLs ensure price accuracy but increase database load and reduce cache effectiveness. Long TTLs improve performance but may serve stale prices during promotions or inventory changes.
Monolithic vs. microservicesSingle pricing engine serviceSeparate services per pricing type (e.g., inventory, promotion)Monolithic approach simplifies development and reduces inter-service communication overhead. Microservices provide better scalability and independent deployment but introduce complexity in managing distributed pricing logic.

Risks and mitigation strategies

RiskImpactMitigation
Price calculation inconsistencyCustomer sees different prices for same item across sessions or devicesImplement comprehensive test suite with price validation scenarios, use deterministic rule execution, and maintain detailed audit logs
High latency under loadPricing requests timeout or cause poor user experienceImplement aggressive caching, use read replicas for database queries, and design price calculation with minimal external dependencies
Currency conversion errorsIncorrect pricing due to exchange rate fluctuations or calculation errorsUse reliable external currency services with fallback mechanisms, implement currency validation checks, and maintain historical exchange rate data
Race conditions in concurrent price updatesInconsistent pricing due to simultaneous rule modificationsImplement optimistic locking on pricing rules, use distributed locks for critical updates, and design idempotent update operations
Cache stampedeCache misses causing database overloadImplement probabilistic early expiry, use distributed locks for cache population, and set up proper cache warming strategies
Rule engine performance degradationSlow price calculation as rule complexity increasesSet up performance monitoring and alerting, implement rule complexity limits, and design rule engine with performance profiling capabilities

Technical debt acknowledged

  • Hardcoded pricing rules: Initial implementation may include hardcoded pricing rules that will need refactoring into a flexible rule engine as business requirements evolve
  • Limited currency support: Current design assumes single currency handling, requiring future refactoring to support multi-currency scenarios
  • Basic caching strategy: Simple TTL-based caching will need enhancement with more sophisticated cache warming and invalidation strategies
  • Monolithic architecture: Pricing engine is currently designed as a monolithic component that will eventually need to be split into microservices for better scalability and independent deployment
  • Limited audit trail: Basic audit logging is implemented but will require enhancement to support comprehensive compliance and debugging requirements
  • No versioning strategy: Current implementation lacks proper version control for pricing rules, which will be critical as business logic evolves over time

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.