Skip to main content

Pricing Calculator Backend System Design

Comprehensive low-level design for a scalable pricing calculator backend with dynamic rule engine and real-time calculation capabilities.

A robust backend architecture for handling complex pricing calculations with support for dynamic rules, real-time updates, and high-concurrency processing across multiple product types and pricing models.

1. Problem Statement & Requirements

Clarifying questions

  1. What is the scope of pricing calculation? Will it handle simple product pricing, complex subscription tiers, or dynamic pricing based on market conditions?
  2. What is the expected scale? How many concurrent price calculations are expected, and what is the peak volume of pricing requests per second?
  3. What consistency requirements exist? Should price changes be immediately visible to all users, or can there be a small delay (e.g., 1-5 minutes)?
  4. What is the read/write ratio? Is pricing data mostly read (e.g., product listings) or frequently updated (e.g., promotional campaigns)?
  5. What latency sensitivity is acceptable? Should pricing be calculated in <10ms for real-time display, or can it tolerate up to 500ms?
  6. What authentication model is in place? Will pricing be exposed to public users or only authenticated service consumers?
  7. Are there external integrations? Will pricing depend on currency exchange rates, third-party APIs, or real-time inventory?

Functional requirements

  • Calculate price for a product or service based on various pricing rules and conditions
  • Support dynamic pricing rules (e.g., time-based, volume discounts, promotional codes)
  • Handle multiple currencies and currency conversion
  • Apply tax calculations based on location or jurisdiction
  • Support price tiers (e.g., bulk discounts, tiered pricing)
  • Support promotional codes and coupon-based discounts
  • Return price breakdown (base price, taxes, discounts, total)
  • Support real-time and batch pricing calculations
  • Handle complex pricing scenarios (e.g., seasonal pricing, promotional campaigns)

Non-functional requirements

  • Latency targets:
    • p50: <50ms
    • p95: <100ms
    • p99: <200ms
  • Throughput: 10,000 concurrent price calculations per second
  • Availability: 99.9% uptime (28.8 minutes of downtime/month)
  • Scalability: Support horizontal scaling across multiple instances
  • Consistency: Eventual consistency for price updates with <5 minute propagation delay

Constraints and assumptions

  • Pricing engine will be part of a larger e-commerce or SaaS platform
  • Price calculation logic is complex and involves multiple business rules
  • Pricing data will be stored in a database and may require caching for performance
  • Currency conversion rates are fetched from external APIs or maintained internally
  • Pricing rules will be configurable and may change frequently
  • System assumes stable network connectivity to external services (e.g., currency APIs)
  • Pricing engine will not handle actual transactions or payment processing

Out of scope

  • Payment processing or transaction handling
  • Real-time inventory updates or stock level validation
  • User session management or authentication
  • Price history tracking or audit logs (limited to price calculation)
  • Complex financial calculations beyond basic tax and discount logic
  • Multi-tenant pricing rule isolation (assumes single tenant or shared rules)
  • Integration with external payment gateways or fraud detection systems

2. API Design

API Style Selection

For a pricing calculator system, I'll select REST API with JSON payloads for the following reasons:

  • Familiarity and wide adoption across teams
  • Clear resource-based semantics (e.g., /pricing-rules, /calculations)
  • Good tooling support for documentation and testing
  • Simpler client implementation compared to GraphQL or gRPC
  • Better cacheability for read-heavy pricing operations

Endpoint Definitions

MethodPathDescriptionAuth
POST/v1/pricing-rulesCreate new pricing ruleJWT + Admin
GET/v1/pricing-rulesList all pricing rulesJWT + Admin
GET/v1/pricing-rules/{id}Get specific pricing ruleJWT + Admin
PUT/v1/pricing-rules/{id}Update pricing ruleJWT + Admin
DELETE/v1/pricing-rules/{id}Delete pricing ruleJWT + Admin
POST/v1/calculationsCalculate price for item(s)JWT + User
GET/v1/calculations/{id}Get calculation resultJWT + User
POST/v1/calculations/previewPreview calculation without persistingJWT + User

Request and Response Schemas

Create Pricing Rule Request

{
  "name": "Bulk Discount Rule",
  "description": "10% discount for orders over 5 items",
  "type": "BULK_DISCOUNT",
  "conditions": {
    "minQuantity": 5,
    "maxQuantity": null,
    "productCategory": "Electronics"
  },
  "discount": {
    "type": "PERCENTAGE",
    "value": 10
  },
  "priority": 1,
  "isActive": true
}

Create Pricing Rule Response

{
  "id": "pr_1234567890",
  "name": "Bulk Discount Rule",
  "description": "10% discount for orders over 5 items",
  "type": "BULK_DISCOUNT",
  "conditions": {
    "minQuantity": 5,
    "maxQuantity": null,
    "productCategory": "Electronics"
  },
  "discount": {
    "type": "PERCENTAGE",
    "value": 10
  },
  "priority": 1,
  "isActive": true,
  "createdAt": "2023-10-01T10:00:00Z",
  "updatedAt": "2023-10-01T10:00:00Z"
}

Calculate Price Request

{
  "items": [
    {
      "productId": "prod_123",
      "quantity": 2,
      "unitPrice": 100.00
    },
    {
      "productId": "prod_456",
      "quantity": 1,
      "unitPrice": 50.00
    }
  ],
  "customerTier": "PREMIUM",
  "currency": "USD"
}

Calculate Price Response

{
  "calculationId": "calc_1234567890",
  "items": [
    {
      "productId": "prod_123",
      "quantity": 2,
      "unitPrice": 100.00,
      "subtotal": 200.00,
      "discountedPrice": 180.00,
      "discountAmount": 20.00
    }
  ],
  "totalAmount": 230.00,
  "currency": "USD",
  "discountsApplied": [
    {
      "ruleId": "pr_1234567890",
      "ruleName": "Bulk Discount Rule",
      "discountType": "PERCENTAGE",
      "discountValue": 10,
      "amount": 20.00
    }
  ],
  "createdAt": "2023-10-01T10:00:00Z"
}

HTTP Status Codes and Error Response Format

Status CodeDescription
200Success (GET, PUT, PATCH)
201Created (POST)
400Bad Request (validation errors, malformed JSON)
401Unauthorized (missing or invalid auth)
403Forbidden (insufficient permissions)
404Not Found (resource doesn't exist)
429Too Many Requests (rate limiting)
500Internal Server Error

Standardized error response format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input provided",
    "details": [
      {
        "field": "quantity",
        "message": "Quantity must be greater than 0"
      }
    ],
    "timestamp": "2023-10-01T10:00:00Z"
  }
}

API Versioning Strategy

Versioning using path prefix: /v1/ for initial version. This approach:

  • Allows independent evolution of APIs
  • Supports backward compatibility through versioned endpoints
  • Enables clean migration paths for breaking changes

Pagination Design

Using cursor-based pagination with after parameter:

  • More consistent and performant than offset-based pagination
  • Handles data changes gracefully (inserts/deletes don't shift page boundaries)
  • Better for large datasets with high cardinality

Authentication and Authorization

Authentication

  • JWT tokens for API authentication (OAuth2 with JWT as bearer token)
  • Token validation using asymmetric signing (RS256)

Authorization

  • Admin role for pricing rule management (POST/PUT/DELETE)
  • User role for price calculation (POST/GET)
  • RBAC model with role-based access control

Rate Limiting

EndpointLimitPeriodEnforcement
/v1/calculations1000 requests/hourHourlyIn-memory + Redis
/v1/pricing-rules100 requests/hourHourlyIn-memory + Redis
/v1/calculations/preview500 requests/hourHourlyIn-memory + Redis

Rate limiting strategy:

  • Token bucket algorithm for burst handling
  • Distributed implementation using Redis to maintain consistency across instances

Idempotency Design

For mutation endpoints (POST /v1/calculations, POST /v1/pricing-rules):

  • Idempotency key header: X-Idempotency-Key
  • Store operation metadata in Redis with TTL of 24 hours
  • If same key exists and operation is identical, return cached result
  • Use UUID as idempotency key for client-generated uniqueness
// Example of idempotency handling in controller
@PostMapping("/calculations")
public ResponseEntity<CalculationResponse> calculatePrice(
    @RequestHeader("X-Idempotency-Key") String idempotencyKey,
    @RequestBody CalculationRequest request) {
    
    // Check if this operation was already processed
    String cachedResult = redisTemplate.opsForValue().get(idempotencyKey);
    if (cachedResult != null) {
        return ResponseEntity.ok(objectMapper.readValue(cachedResult, CalculationResponse.class));
    }
    
    // Process the calculation
    CalculationResponse response = pricingService.calculate(request);
    
    // Cache the result for idempotency
    redisTemplate.opsForValue().set(idempotencyKey, objectMapper.writeValueAsString(response), 
                                   Duration.ofHours(24));
    
    return ResponseEntity.ok(response);
}

3. Data Model Design

Entity-Relationship Diagram

erDiagram
    PRODUCT {
        string id PK
        string name
        string description
        string category
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }

    PRICE_RULE {
        string id PK
        string name
        string description
        string rule_type
        jsonb conditions
        jsonb actions
        integer priority
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }

    PRICE_TIER {
        string id PK
        string price_rule_id FK
        string name
        jsonb conditions
        decimal amount
        timestamp created_at
        timestamp updated_at
    }

    PRODUCT_PRICE {
        string id PK
        string product_id FK
        string price_rule_id FK
        decimal amount
        timestamp effective_from
        timestamp effective_to
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }

    PRICE_CALCULATION_CONTEXT {
        string id PK
        jsonb context_data
        timestamp created_at
    }

    PRODUCT ||--o{ PRODUCT_PRICE : "has"
    PRICE_RULE ||--o{ PRICE_TIER : "has"
    PRODUCT_PRICE ||--o{ PRICE_CALCULATION_CONTEXT : "uses"

Table Schema Definitions

-- Products table
CREATE TABLE products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    category VARCHAR(100),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Price rules table
CREATE TABLE price_rules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    rule_type VARCHAR(50) NOT NULL, -- 'tiered', 'discount', 'surcharge'
    conditions JSONB NOT NULL,
    actions JSONB NOT NULL,
    priority INTEGER NOT NULL DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Price tiers table
CREATE TABLE price_tiers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    price_rule_id UUID NOT NULL REFERENCES price_rules(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    conditions JSONB NOT NULL,
    amount DECIMAL(15,2) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Product prices table
CREATE TABLE product_prices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
    price_rule_id UUID REFERENCES price_rules(id),
    amount DECIMAL(15,2) NOT NULL,
    effective_from TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    effective_to TIMESTAMP WITH TIME ZONE,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Price calculation contexts table
CREATE TABLE price_calculation_contexts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    context_data JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for performance optimization
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_active ON products(is_active);
CREATE INDEX idx_price_rules_priority ON price_rules(priority);
CREATE INDEX idx_price_rules_active ON price_rules(is_active);
CREATE INDEX idx_product_prices_product_id ON product_prices(product_id);
CREATE INDEX idx_product_prices_active ON product_prices(is_active);
CREATE INDEX idx_product_prices_effective_date ON product_prices(effective_from, effective_to);
CREATE INDEX idx_price_tiers_price_rule_id ON price_tiers(price_rule_id);

Primary Key Strategy

UUIDs are used as primary keys for all tables. This choice provides:

  1. Distributed system compatibility - No coordination required between services for ID generation
  2. No hotspots - Eliminates contention in database clusters
  3. Scalability - Supports horizontal sharding and cross-service references
  4. Security - Prevents enumeration attacks by making IDs unpredictable

Index Design Justification

IndexFieldsTypePurpose
idx_products_categorycategoryStandardFast filtering by product categories
idx_products_activeis_activeStandardEfficiently filter active products
idx_price_rules_prioritypriorityStandardFast rule selection by priority
idx_price_rules_activeis_activeStandardEfficiently filter active rules
idx_product_prices_product_idproduct_idStandardFast lookup of prices by product
idx_product_prices_activeis_activeStandardEfficiently filter active price records
idx_product_prices_effective_dateeffective_from, effective_toCompositeFast time-range queries for price validity
idx_price_tiers_price_rule_idprice_rule_idStandardFast lookup of tiers by rule

Data Partitioning Strategy

Sharding by Product Category:

  • Partition products and their associated price data by category
  • Each category gets its own database shard or tablespace
  • This enables:
    • Isolation of high-volume categories
    • Independent scaling per category
    • Reduced contention in hot product categories

Soft Delete vs. Hard Delete

Soft delete is used for all tables with is_active boolean flag:

  1. Auditability: Historical price changes can be traced
  2. Recovery capability: Data can be restored if accidentally deleted
  3. Referential integrity: Price rules and tiers remain valid in historical contexts
  4. Compliance: Supports regulatory requirements for data retention

Audit Trail Design

All tables include created_at and updated_at timestamps with:

  • Created by: Not stored in DB (handled at application level via auth context)
  • Updated by: Not stored in DB (handled at application level via auth context)
  • Change tracking: Implemented through audit logs table or triggers if needed

Data Archival Strategy

  1. Active price rules: Keep in primary database
  2. Historical price data: Archive to time-series or data warehouse after 1 year
  3. Inactive rules: Move to read-only archive database after 2 years
  4. Soft-deleted records: Retain for 90 days before permanent deletion
-- Example archiving query (to be run nightly)
INSERT INTO price_rule_archives 
SELECT * FROM price_rules WHERE updated_at < NOW() - INTERVAL '1 year';

4. Class Design & Service Layer

classDiagram
    class PricingService {
        +calculatePrice(PriceRequest) PriceResponse
        +validateRequest(PriceRequest) ValidationResult
    }
    
    class PriceCalculator {
        +calculate(PriceContext) BigDecimal
    }
    
    class PriceRule {
        <<interface>>
        +apply(PriceContext) BigDecimal
    }
    
    class TieredPricingRule {
        +apply(PriceContext) BigDecimal
    }
    
    class DiscountRule {
        +apply(PriceContext) BigDecimal
    }
    
    class PriceRequest {
        customerType: CustomerType
        items: List~PriceItem~
        currency: String
        timezone: ZoneId
    }
    
    class PriceResponse {
        totalAmount: BigDecimal
        breakdown: List~PriceBreakdown~
        currency: String
        timestamp: Instant
    }
    
    class PriceContext {
        customerType: CustomerType
        items: List~PriceItem~
        currency: String
        timezone: ZoneId
        appliedRules: List~PriceRule~
    }
    
    class PriceItem {
        productId: String
        quantity: int
        unitPrice: BigDecimal
    }
    
    class PriceBreakdown {
        item: PriceItem
        subtotal: BigDecimal
        discounts: List~Discount~
        finalAmount: BigDecimal
    }
    
    class Discount {
        type: DiscountType
        amount: BigDecimal
        description: String
    }
    
    class PriceRuleEngine {
        +applyRules(PriceContext) PriceContext
    }
    
    PricingService --> PriceCalculator : uses
    PriceCalculator --> PriceRuleEngine : uses
    PriceRuleEngine --> PriceRule : implements
    PriceRule <|-- TieredPricingRule : implements
    PriceRule <|-- DiscountRule : implements
    PricingService --> PriceRequest : accepts
    PricingService --> PriceResponse : returns
stateDiagram-v2
    [*] --> Active
    Active --> Inactive : customer_inactive
    Active --> Suspended : customer_suspended
    Active --> Cancelled : customer_cancelled
    Inactive --> Active : customer_reactivated
    Suspended --> Active : suspension_lifted
    Suspended --> Cancelled : suspension_expired
    Cancelled --> [*]

Service Layer Overview

The pricing calculator service layer is designed around a rule-based calculation engine that supports extensibility and maintainability. The core components include:

  1. PricingService: Main entry point for price calculation operations
  2. PriceCalculator: Core business logic engine that orchestrates rule application
  3. PriceRuleEngine: Manages the execution of pricing rules in proper sequence
  4. PriceRule: Interface defining rule behavior for extensibility
  5. PriceContext: Immutable context object carrying all relevant calculation data

Business Logic Patterns

Strategy Pattern Implementation

The PriceRule interface and its implementations (TieredPricingRule, DiscountRule) demonstrate the Strategy pattern for flexible rule application:

public interface PriceRule {
    BigDecimal apply(PriceContext context);
}

public class TieredPricingRule implements PriceRule {
    private final TieredPricingConfig config;
    
    public TieredPricingRule(TieredPricingConfig config) {
        this.config = config;
    }
    
    @Override
    public BigDecimal apply(PriceContext context) {
        // Implementation logic for tiered pricing calculation
        return calculateTieredPrice(context.getItems(), config);
    }
    
    private BigDecimal calculateTieredPrice(List<PriceItem> items, TieredPricingConfig config) {
        // Tiered pricing logic here
        return BigDecimal.ZERO;
    }
}

public class DiscountRule implements PriceRule {
    private final DiscountConfig config;
    
    public DiscountRule(DiscountConfig config) {
        this.config = config;
    }
    
    @Override
    public BigDecimal apply(PriceContext context) {
        // Implementation logic for discount calculation
        return calculateDiscount(context.getItems(), config);
    }
    
    private BigDecimal calculateDiscount(List<PriceItem> items, DiscountConfig config) {
        // Discount calculation logic here
        return BigDecimal.ZERO;
    }
}

Critical-Path Method Implementation

public PriceResponse calculatePrice(PriceRequest request) {
    // Validate input
    ValidationResult validation = validateRequest(request);
    if (!validation.isValid()) {
        throw new ValidationException("Invalid price request", validation.getErrors());
    }

    // Create immutable context
    PriceContext context = PriceContext.builder()
        .customerType(request.getCustomerType())
        .items(request.getItems())
        .currency(request.getCurrency())
        .timezone(request.getTimezone())
        .build();

    // Apply all pricing rules in sequence
    try {
        // Start transaction boundary - begin DB transaction for consistency
        TransactionStatus transaction = transactionManager.getTransaction(
            new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_COMMITTED)
        );

        try {
            // Apply pricing rules
            PriceContext processedContext = priceRuleEngine.applyRules(context);
            
            // Calculate final total
            BigDecimal totalAmount = processedContext.getItems().stream()
                .map(PriceItem::getFinalAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

            // Create response
            PriceResponse response = PriceResponse.builder()
                .totalAmount(totalAmount)
                .breakdown(generateBreakdown(processedContext))
                .currency(request.getCurrency())
                .timestamp(Instant.now())
                .build();

            // Publish price calculation event for audit and analytics
            eventPublisher.publish(new PriceCalculatedEvent(
                response, 
                processedContext.getAppliedRules().stream()
                    .map(PriceRule::getClass)
                    .collect(Collectors.toList())
            ));

            // Commit transaction
            transactionManager.commit(transaction);
            return response;
            
        } catch (Exception e) {
            // Rollback on failure
            transactionManager.rollback(transaction);
            throw new PricingCalculationException("Failed to calculate price", e);
        }
    } catch (Exception e) {
        throw new PricingCalculationException("Transaction failure during price calculation", e);
    }
}

Transaction Boundary Design

The transaction boundary encompasses:

  1. Consistent state: All pricing rule calculations happen atomically
  2. Data integrity: Ensures no partial updates occur during calculation
  3. Isolation level: READ_COMMITTED to prevent dirty reads while maintaining performance
  4. Resource management: Proper cleanup of database connections and locks

Service Interface Contracts

public interface PricingService {
    PriceResponse calculatePrice(PriceRequest request);
    ValidationResult validateRequest(PriceRequest request);
}

Saga Pattern Consideration

For complex pricing scenarios involving external systems (e.g., currency conversion, third-party rate APIs), a saga pattern would be implemented:

public class PricingSaga {
    private final List<SagaStep> steps;
    
    public void execute(PriceRequest request) {
        // Step 1: Validate request
        // Step 2: Fetch external rates if needed  
        // Step 3: Apply local pricing rules
        // Step 4: Commit or rollback based on success/failure
    }
}

Domain Object Design

public class PriceRequest {
    private final CustomerType customerType;
    private final List<PriceItem> items;
    private final String currency;
    private final ZoneId timezone;
    
    // Constructor, getters, equals, hashCode
}

public class PriceItem {
    private final String productId;
    private final int quantity;
    private final BigDecimal unitPrice;
    private final BigDecimal subtotal;
    private final List<Discount> discounts;
    
    // Constructor, getters, equals, hashCode
}

public class PriceContext {
    private final CustomerType customerType;
    private final List<PriceItem> items;
    private final String currency;
    private final ZoneId timezone;
    private final List<PriceRule> appliedRules;
    
    // Constructor, getters, builder pattern
}

5. Concurrency & Thread Safety

Concurrency Model

The pricing calculator system will use a thread-safe, asynchronous processing model with the following characteristics:

  • Java's CompletableFuture and Reactor framework for non-blocking operations
  • Thread pool size tuned to CPU cores + I/O bound work (typically 2x CPU cores for I/O-heavy pricing calculations)
  • Single-threaded execution context per price calculation request to maintain consistency
  • Asynchronous processing for concurrent price calculations without blocking the main request thread

Race Condition Prevention Strategy

Race conditions are mitigated through:

  1. Immutable pricing objects - All price calculation components are immutable to prevent concurrent modification
  2. Atomic operations for cache updates and state transitions using java.util.concurrent.atomic classes
  3. Read-Write locks for shared pricing rule collections to allow concurrent reads while blocking writes
  4. Database-level atomic operations for price updates using optimistic locking with version fields

Optimistic vs. Pessimistic Locking Selection

Optimistic locking is selected for pricing calculations because:

  • Price calculation is primarily read-heavy with infrequent updates
  • Concurrent price requests are expected to rarely conflict
  • Reduces lock contention and improves throughput
  • Pricing rules are updated less than 10 times per hour in typical usage

Implementation pattern:

// Using version field for optimistic locking
@Version
private Long version;

// In service layer:
try {
    priceRuleRepository.save(priceRule);
} catch (OptimisticLockException e) {
    // Retry logic or conflict resolution
    throw new PricingCalculationException("Concurrent update detected");
}

Deadlock Prevention Strategy

Deadlock prevention mechanisms:

  1. Lock ordering - All pricing rule locks acquired in consistent order (by ID or name)
  2. Timeout configuration - 500ms timeout for lock acquisition to prevent indefinite waits
  3. Hierarchical locking - Price rule locks acquired before calculation context locks
  4. Thread pool size limiting - Controlled concurrent price calculations to prevent resource exhaustion

Idempotency Implementation for Concurrent Duplicate Requests

Idempotency is handled through:

  1. Idempotency keys in request headers (X-Idempotency-Key)
  2. Cache-based idempotency - Store calculation results with key for 5 minutes
  3. Request deduplication - Concurrent requests with same key wait for first calculation to complete
public class PricingIdempotencyService {
    private final Map<String, CompletableFuture<PriceCalculationResult>> activeCalculations = 
        new ConcurrentHashMap<>();
    
    public CompletableFuture<PriceCalculationResult> calculateWithIdempotency(
            String idempotencyKey, 
            PriceCalculationRequest request) {
        
        return activeCalculations.computeIfAbsent(idempotencyKey, key -> {
            // First request starts calculation
            return CompletableFuture.supplyAsync(() -> calculatePrice(request))
                .whenComplete((result, error) -> {
                    // Remove from active calculations after completion
                    activeCalculations.remove(key);
                });
        }).thenCompose(result -> {
            // If already completed, return cached result
            return CompletableFuture.completedFuture(result);
        });
    }
}

6. Sequence Diagrams for Key Flows

sequenceDiagram
    participant C as Client
    participant API as PricingCalculatorAPI
    participant Service as PricingService
    participant DB as Database
    participant Cache as RedisCache
    participant EventPublisher as EventPublisher

    C->>API: POST /v1/pricing/calculate
    API->>Service: calculatePrice(request)
    Service->>Cache: get(priceRules)
    Cache-->>Service: priceRules
    alt price rules not in cache
        Service->>DB: fetchActivePriceRules()
        DB-->>Service: priceRules
        Service->>Cache: put(priceRules)
    end
    Service->>Service: applyRules(request)
    Service->>EventPublisher: publish(priceCalculated)
    EventPublisher-->>Service: ack
    Service-->>API: response
    API-->>C: 200 OK
sequenceDiagram
    participant C as Client
    participant API as PricingCalculatorAPI
    participant Service as PricingService
    participant DB as Database
    participant Cache as RedisCache
    participant EventPublisher as EventPublisher

    C->>API: POST /v1/pricing/calculate
    API->>Service: calculatePrice(request)
    Service->>Cache: get(priceRules)
    Cache-->>Service: cache miss
    Service->>DB: fetchActivePriceRules()
    DB-->>Service: DB error
    Service-->>API: 500 Internal Server Error
    API-->>C: 500 Internal Server Error
sequenceDiagram
    participant C as Client
    participant API as PricingCalculatorAPI
    participant Service as PricingService
    participant DB as Database
    participant Cache as RedisCache
    participant EventPublisher as EventPublisher

    C->>API: POST /v1/pricing/calculate
    API->>Service: calculatePrice(request)
    Service->>Cache: get(priceRules)
    Cache-->>Service: priceRules
    Service->>Service: applyRules(request)
    Service->>EventPublisher: publish(priceCalculated)
    EventPublisher-->>Service: error
    Service-->>API: 500 Internal Server Error
    API-->>C: 500 Internal Server Error

7. Caching Design

Cache Strategy Selection

For the pricing calculator, we'll use cache-aside pattern with a time-to-live (TTL) based eviction policy. This approach provides the best balance between performance and data consistency for a pricing system where prices may change but don't require immediate consistency.

Cache Key Design

Cache keys will follow a structured namespace pattern:

pricing:{product_id}:{currency}:{effective_date}:{rules_hash}

Example key: pricing:PROD-123:USD:2023-12-01:abc123def456

TTL Strategy

Data TypeTTLReason
Product prices1 hourPrices may change daily but not immediately
Currency rates30 minutesExchange rates update frequently
Pricing rules24 hoursRules change less frequently than prices
User-specific pricing15 minutesPersonalized discounts may expire quickly

Cache Eviction Policy

We'll use LRU (Least Recently Used) eviction policy with a maximum cache size of 100MB. This ensures that the most frequently accessed pricing calculations remain in cache while preventing memory bloat from stale entries.

Cache Stampede Prevention

To prevent cache stampedes:

  1. Probabilistic early expiry: Set TTL to 75% of actual TTL (e.g., 45 minutes for 1-hour entries)
  2. Distributed locks: Use Redis distributed locks when cache misses occur to prevent multiple concurrent recalculations
  3. Cache warming: Pre-warm frequently accessed price calculations during low-traffic periods

Distributed Cache Architecture

We'll implement a Redis cluster with:

  • 3 master nodes for write operations
  • 3 replica nodes for read operations
  • Automatic failover with Redis Sentinel
  • Cross-zone replication for high availability

Cache Invalidation Strategy

We'll implement event-driven invalidation using a publish-subscribe model:

  1. Write operations: When pricing rules or product data changes, publish invalidation events to Redis channels
  2. Cache invalidation: Consumers listen for these events and invalidate relevant cache entries
  3. Graceful degradation: If cache invalidation fails, fallback to TTL-based expiration
// Example cache invalidation logic in Java
@Component
public class PricingCacheInvalidator {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @EventListener
    public void handlePricingRuleUpdate(PricingRuleUpdatedEvent event) {
        String cacheKeyPattern = "pricing:*:" + event.getRuleId() + ":*";
        Set<String> keys = redisTemplate.keys(cacheKeyPattern);
        
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }
    
    @EventListener
    public void handleProductUpdate(ProductUpdatedEvent event) {
        String cacheKeyPattern = "pricing:" + event.getProductId() + ":*:*";
        Set<String> keys = redisTemplate.keys(cacheKeyPattern);
        
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }
}

Cache Read-Through Pattern

@Service
public class PricingCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private PricingCalculator pricingCalculator;
    
    public Price calculatePriceWithCaching(PricingContext context) {
        String cacheKey = buildCacheKey(context);
        
        // Try to get from cache first
        Price cachedPrice = (Price) redisTemplate.opsForValue().get(cacheKey);
        
        if (cachedPrice != null) {
            return cachedPrice;
        }
        
        // Cache miss - acquire distributed lock to prevent stampede
        String lockKey = "lock:" + cacheKey;
        boolean acquired = acquireDistributedLock(lockKey, 5000); // 5 second timeout
        
        try {
            // Double-check cache after acquiring lock
            cachedPrice = (Price) redisTemplate.opsForValue().get(cacheKey);
            if (cachedPrice != null) {
                return cachedPrice;
            }
            
            // Calculate price
            Price calculatedPrice = pricingCalculator.calculate(context);
            
            // Store in cache with TTL
            redisTemplate.opsForValue().set(
                cacheKey, 
                calculatedPrice, 
                1, TimeUnit.HOURS
            );
            
            return calculatedPrice;
        } finally {
            if (acquired) {
                releaseDistributedLock(lockKey);
            }
        }
    }
    
    private String buildCacheKey(PricingContext context) {
        return "pricing:" + 
               context.getProductId() + ":" +
               context.getCurrency() + ":" +
               context.getEffectiveDate().toString() + ":" +
               generateRuleHash(context.getRules());
    }
}

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 persistence concerns. Each entity has its own repository interface with standardized CRUD operations.

public interface PricingRuleRepository {
    Optional<PricingRule> findById(UUID id);
    List<PricingRule> findAll();
    List<PricingRule> findByType(PricingRuleType type);
    PricingRule save(PricingRule rule);
    void delete(UUID id);
    boolean existsById(UUID id);
}

public interface PriceCalculationRepository {
    Optional<PriceCalculation> findByReferenceId(String referenceId);
    PriceCalculation save(PriceCalculation calculation);
    List<PriceCalculation> findByEntityId(String entityId);
}

ORM vs Raw Query Decisions

For pricing calculations, we use a hybrid approach:

  • JPA/Hibernate for complex domain entities (PricingRule, PriceCalculation) due to its ability to handle complex relationships and lazy loading
  • Raw SQL for performance-critical price calculation queries where we need fine-grained control over query execution and optimization
@Repository
public class PricingRuleRepositoryImpl implements PricingRuleRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public Optional<PricingRule> findById(UUID id) {
        return Optional.ofNullable(entityManager.find(PricingRule.class, id));
    }
    
    @Override
    public List<PricingRule> findByType(PricingRuleType type) {
        return entityManager.createQuery(
            "SELECT r FROM PricingRule r WHERE r.type = :type ORDER BY r.priority DESC", 
            PricingRule.class)
            .setParameter("type", type)
            .getResultList();
    }
    
    @Override
    public List<PricingRule> findAll() {
        return entityManager.createQuery("SELECT r FROM PricingRule r", PricingRule.class)
            .getResultList();
    }
    
    @Override
    public PricingRule save(PricingRule rule) {
        if (rule.getId() == null) {
            entityManager.persist(rule);
        } else {
            entityManager.merge(rule);
        }
        return rule;
    }
    
    @Override
    public void delete(UUID id) {
        PricingRule rule = entityManager.find(PricingRule.class, id);
        if (rule != null) {
            entityManager.remove(rule);
        }
    }
    
    // Raw SQL for performance-critical price calculation
    @PersistenceContext
    private EntityManager entityManager;
    
    public BigDecimal calculatePriceRawQuery(
            String entityId, 
            LocalDateTime effectiveTime,
            Map<String, Object> context) {
        
        String sql = """
            SELECT SUM(pr.amount) as total_price
            FROM pricing_rules pr
            WHERE pr.entity_id = :entityId 
            AND pr.effective_from <= :effectiveTime
            AND pr.effective_to >= :effectiveTime
            AND pr.status = 'ACTIVE'
            ORDER BY pr.priority DESC
            """;
            
        Query query = entityManager.createNativeQuery(sql);
        query.setParameter("entityId", entityId);
        query.setParameter("effectiveTime", effectiveTime);
        
        Object result = query.getSingleResult();
        return result != null ? new BigDecimal(result.toString()) : BigDecimal.ZERO;
    }
}

Read vs Write Separation

We implement CQRS (Command Query Responsibility Segregation) for the pricing system to handle read-heavy workloads efficiently:

@Repository
public class PricingReadRepository {
    private final JdbcTemplate jdbcTemplate;
    
    public List<PricingRule> getActiveRulesByEntity(String entityId) {
        String sql = """
            SELECT * FROM pricing_rules 
            WHERE entity_id = ? AND status = 'ACTIVE' 
            ORDER BY priority DESC
            """;
            
        return jdbcTemplate.query(sql, new Object[]{entityId}, 
            (rs, rowNum) -> mapToPricingRule(rs));
    }
    
    public PriceCalculationSummary getCalculationSummary(String referenceId) {
        String sql = """
            SELECT reference_id, calculated_price, currency, created_at 
            FROM price_calculations 
            WHERE reference_id = ?
            """;
            
        return jdbcTemplate.queryForObject(sql, new Object[]{referenceId}, 
            (rs, rowNum) -> new PriceCalculationSummary(
                rs.getString("reference_id"),
                rs.getBigDecimal("calculated_price"),
                rs.getString("currency"),
                rs.getTimestamp("created_at").toLocalDateTime()
            ));
    }
}

@Repository
public class PricingWriteRepository {
    private final EntityManager entityManager;
    
    public PriceCalculation saveCalculation(PriceCalculation calculation) {
        entityManager.persist(calculation);
        return calculation;
    }
    
    public void updateCalculation(PriceCalculation calculation) {
        entityManager.merge(calculation);
    }
}

Connection Pool Configuration

Connection pool is configured with:

  • Max pool size: 30 (based on concurrent request capacity)
  • Min idle connections: 5
  • Connection timeout: 30 seconds
  • Validation query: SELECT 1
# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      minimum-idle: 5
      connection-timeout: 30000
      validation-timeout: 5000
      idle-timeout: 600000
      max-lifetime: 1800000
      pool-name: PricingPool

N+1 Query Prevention Strategy

We implement eager loading and batch fetching to prevent N+1 query problems:

@Entity
@Table(name = "pricing_rules")
@NamedEntityGraph(
    name = "PricingRule.withDetails",
    attributeNodes = @NamedAttributeNode(value = "conditions", fetchType = FetchType.EAGER),
    subgraphs = @NamedSubgraph(
        name = "PricingRule.conditions",
        attributeNodes = @NamedAttributeNode(value = "ruleConditions", fetchType = FetchType.EAGER)
    ),
    subgraphs = @NamedSubgraph(
        name = "PricingRule.appliedRules",
        attributeNodes = @NamedAttributeNode(value = "appliedRules", fetchType = FetchType.EAGER)
    )
)
public class PricingRule {
    @Id
    private UUID id;
    
    @OneToMany(mappedBy = "pricingRule", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<RuleCondition> conditions = new ArrayList<>();
    
    @OneToMany(mappedBy = "pricingRule", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<AppliedRule> appliedRules = new ArrayList<>();
    
    // ... other fields
}

@Repository
public class PricingRuleRepositoryImpl {
    
    public List<PricingRule> findActiveRulesWithConditions(String entityId) {
        String jpql = """
            SELECT r FROM PricingRule r 
            JOIN FETCH r.conditions c 
            WHERE r.entityId = :entityId 
            AND r.status = 'ACTIVE'
            ORDER BY r.priority DESC
            """;
            
        return entityManager.createQuery(jpql, PricingRule.class)
            .setParameter("entityId", entityId)
            .getResultList();
    }
}

Batch Operation Design

Batch operations are implemented for bulk rule creation and price calculation:

@Repository
public class BatchPricingRepository {
    
    public void batchSaveRules(List<PricingRule> rules) {
        String sql = """
            INSERT INTO pricing_rules (id, entity_id, type, amount, priority, status, effective_from, effective_to)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """;
            
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                PricingRule rule = rules.get(i);
                ps.setObject(1, rule.getId());
                ps.setString(2, rule.getEntityId());
                ps.setString(3, rule.getType().name());
                ps.setBigDecimal(4, rule.getAmount());
                ps.setInt(5, rule.getPriority());
                ps.setString(6, rule.getStatus().name());
                ps.setTimestamp(7, Timestamp.valueOf(rule.getEffectiveFrom()));
                ps.setTimestamp(8, Timestamp.valueOf(rule.getEffectiveTo()));
            }
            
            @Override
            public int getBatchSize() {
                return rules.size();
            }
        });
    }
    
    public List<PriceCalculation> batchCalculatePrices(List<String> entityIds, LocalDateTime timestamp) {
        String sql = """
            SELECT * FROM price_calculations 
            WHERE entity_id IN (?) AND calculated_at = ?
            """;
            
        return jdbcTemplate.query(sql, new Object[]{String.join(",", entityIds), timestamp}, 
            (rs, rowNum) -> mapToPriceCalculation(rs));
    }
}

9. Asynchronous Processing & Messaging

Event-driven boundaries in this feature

The pricing calculator system will use a hybrid synchronous/asynchronous approach. The core price calculation will be performed synchronously to ensure immediate response for client requests. However, certain operations like price history tracking, audit logging, and promotional rule updates will be handled asynchronously to maintain system responsiveness and decouple components.

Key async boundaries:

  • Price change notifications: When a price is updated, publish to a price.changed topic
  • Promotional rule evaluation: When promotions change or new promotions are added, trigger price recalculation via async event processing
  • Historical price tracking: Store price history snapshots asynchronously to avoid blocking primary calculation flow

Message schema design

{
  "version": "1.0",
  "event_type": "price.changed",
  "timestamp": "2023-12-01T10:30:00Z",
  "payload": {
    "product_id": "prod_12345",
    "old_price": 99.99,
    "new_price": 89.99,
    "currency": "USD",
    "effective_date": "2023-12-01T10:30:00Z",
    "rules_applied": [
      {
        "rule_id": "promo_abc123",
        "description": "Seasonal discount",
        "discount_amount": 10.00
      }
    ]
  },
  "source": "pricing-service",
  "correlation_id": "req_abc123xyz"
}

Dead letter queue strategy and poison message handling

All asynchronous price calculation events will be processed through a dead-letter queue (DLQ) system. Messages that fail more than 3 times will be moved to DLQ with metadata indicating failure reason and retry count. A separate monitoring system will alert on DLQ contents.

Poison message handling:

  • Messages failing more than 3 attempts are routed to DLQ
  • DLQ messages are flagged for manual review and potential reprocessing
  • Failed messages include original payload, error details, and retry count in DLQ metadata

At-least-once vs. exactly-once delivery

At-least-once delivery is chosen for price calculation events because:

  • Price consistency is more important than avoiding duplicate calculations
  • The system can handle duplicate price change notifications gracefully
  • Performance impact of at-least-once is minimal compared to the complexity of exactly-once

Retry policy for consumers

public class PriceChangeEventHandler {
    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_DELAY_MS = 1000;
    private static final double BACKOFF_MULTIPLIER = 2.0;
    
    public void handlePriceChange(PriceChangeEvent event) {
        int attempts = 0;
        while (attempts < MAX_RETRIES) {
            try {
                processPriceChange(event);
                return;
            } catch (Exception e) {
                attempts++;
                if (attempts >= MAX_RETRIES) {
                    // Route to DLQ
                    handleDeadLetter(event, e);
                    break;
                }
                
                long delay = (long) (INITIAL_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attempts - 1));
                // Add jitter (±25%)
                delay = (long) (delay * (0.75 + Math.random() * 0.5));
                
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted during retry", ie);
                }
            }
        }
    }
}

Outbox pattern for reliable event publishing

Outbox pattern is implemented to ensure price change events are published reliably:

public class PriceChangeOutbox {
    private final JdbcTemplate jdbcTemplate;
    
    @Transactional
    public void publishPriceChange(PriceChangeEvent event) {
        // Store event in outbox table
        String sql = "INSERT INTO price_change_outbox (event_data, status, created_at) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, 
            objectMapper.writeValueAsString(event), 
            "PENDING", 
            LocalDateTime.now());
        
        // Commit transaction - this ensures the event is stored before publishing
    }
    
    @Scheduled(fixedDelay = 5000) // Every 5 seconds
    public void processOutbox() {
        List<PriceChangeOutboxEntry> pendingEvents = getPendingEvents();
        for (PriceChangeOutboxEntry entry : pendingEvents) {
            try {
                // Publish to message broker
                messageBroker.publish("price.changed", entry.getEventData());
                
                // Mark as processed
                updateEventStatus(entry.getId(), "PROCESSED");
            } catch (Exception e) {
                // Mark as failed for retry
                updateEventStatus(entry.getId(), "FAILED");
            }
        }
    }
}

Sequence diagram for async flows

sequenceDiagram
    participant Client
    participant PricingService
    participant EventPublisher
    participant MessageBroker
    participant PriceHistoryService
    participant DLQHandler
    
    Client->>PricingService: CalculatePriceRequest
    PricingService->>PricingService: ApplyRules()
    PricingService->>EventPublisher: StorePriceChangeEvent
    EventPublisher->>MessageBroker: Publish price.changed event
    MessageBroker->>PriceHistoryService: Consume price.changed
    PriceHistoryService->>PriceHistoryService: StoreHistoricalRecord()
    MessageBroker->>PricingService: Consume price.changed (retry)
    PricingService->>PricingService: HandleDuplicateEvent()
    
    loop Retry attempts (3)
        MessageBroker->>PricingService: Consume price.changed
        PricingService->>PricingService: HandleDuplicateEvent()
        PricingService->>PricingService: RetryLogic()
    end
    
    alt Retry limit exceeded
        MessageBroker->>DLQHandler: Route to DLQ
        DLQHandler->>DLQHandler: AlertOnFailedEvent()
    end
    
    PricingService-->>Client: PriceResponse

10. Error Handling Design

Error Classification Hierarchy

The pricing calculator system follows a structured error classification to enable proper handling and monitoring:

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

Error Propagation Strategy

  • Validation errors are handled at the API boundary and immediately returned to client
  • Business logic errors are caught at service layer and converted to appropriate HTTP status codes
  • Infrastructure errors are logged and potentially retried before being escalated
  • System errors are bubbled up to global error handler for centralized logging and response formatting

Retry Strategy with Exponential Backoff and Jitter

For retryable infrastructure errors, the system implements exponential backoff with jitter:

public class RetryConfig {
    private static final int MAX_RETRIES = 3;
    private static final long BASE_DELAY_MS = 1000;
    private static final double JITTER_FACTOR = 0.2;
    
    public static long calculateDelay(int attempt) {
        long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempt - 1));
        long jitter = (long) (delay * JITTER_FACTOR * (Math.random() - 0.5) * 2);
        return delay + jitter;
    }
}

Circuit Breaker Design

The pricing calculator implements a circuit breaker pattern for external service dependencies:

public class PricingCircuitBreaker {
    private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("pricing-service");
    
    public <T> T executeWithCircuitBreaker(Supplier<T> operation) {
        return circuitBreaker.executeSupplier(operation);
    }
    
    public void onCallSuccess() {
        circuitBreaker.recordSuccess();
    }
    
    public void onCallFailure() {
        circuitBreaker.recordFailure();
    }
}

Configuration:

  • Failure threshold: 5 consecutive failures
  • Timeout: 30 seconds
  • Half-open state timeout: 60 seconds

Fallback Behavior Design

For external service dependencies, fallbacks are implemented:

public class PricingFallbackHandler {
    private final Map<String, BigDecimal> fallbackPriceCache = new ConcurrentHashMap<>();
    
    public BigDecimal getFallbackPrice(String productId) {
        return fallbackPriceCache.computeIfAbsent(productId, 
            key -> calculateDefaultPrice(key));
    }
    
    private BigDecimal calculateDefaultPrice(String productId) {
        // Return default price based on product category or static mapping
        return new BigDecimal("10.00");
    }
}

Error Logging Format and Alerting Thresholds

Structured logging follows this schema:

{
  "timestamp": "2023-10-15T14:30:45.123Z",
  "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "span_id": "f0e9d8c7-b6a5-4321-0fed-cba987654321",
  "user_id": "user-123",
  "entity_id": "product-456",
  "error_code": "PRICING_CALCULATION_ERROR",
  "error_message": "Failed to calculate price for product-456",
  "error_type": "BusinessLogicException",
  "stack_trace": "...",
  "request_context": {
    "product_id": "product-456",
    "quantity": 2,
    "currency": "USD"
  }
}

Alerting thresholds:

  • Warn: More than 10 pricing calculation errors per hour
  • Critical: More than 50 pricing calculation errors per hour or 5 consecutive failures to external services
  • On-call escalation: Any internal server errors or circuit breaker tripped

Error Response Format

Standardized JSON error response:

{
  "error": {
    "code": "PRICING_CALCULATION_ERROR",
    "message": "Failed to calculate price for product-456",
    "timestamp": "2023-10-15T14:30:45.123Z",
    "request_id": "req-789"
  }
}

Retryable vs Non-Retryable Errors

Error TypeRetryableReason
ValidationExceptionNoClient error, no point in retry
BusinessLogicExceptionNoBusiness rule violation
DatabaseExceptionYesTransient DB issues
ExternalServiceExceptionYesNetwork or service availability
CacheExceptionYesCache system issues
TimeoutExceptionYesTemporary network latency

Error Handling in Service Layer

public class PricingService {
    private final ExternalPricingClient externalClient;
    private final CircuitBreaker circuitBreaker;
    
    public BigDecimal calculatePrice(PricingRequest request) {
        try {
            return circuitBreaker.executeSupplier(() -> {
                // Business logic validation
                validateRequest(request);
                
                // Calculate price with fallback for external calls
                return calculateWithExternalService(request);
            });
        } catch (ValidationException e) {
            // Log and rethrow validation errors immediately
            logger.warn("Validation error in pricing calculation", e);
            throw e;
        } catch (InfrastructureException e) {
            // Log and potentially retry
            logger.error("Infrastructure error in pricing calculation", e);
            throw new PricingCalculationException("Pricing calculation failed", e);
        } catch (Exception e) {
            // Handle unexpected errors
            logger.error("Unexpected error in pricing calculation", e);
            throw new InternalServerException("Internal system error", e);
        }
    }
}

11. Security Design

Input Validation and Sanitization Strategy

All API inputs undergo strict validation using Jakarta Bean Validation (JSR-380) with custom constraints for business logic. Input sanitization is performed using OWASP Java HTML Sanitizer for any text fields that might contain user-generated content. Validation includes:

  • Type checking (e.g., numeric inputs must be valid doubles)
  • Range validation (e.g., price cannot be negative)
  • Format validation (e.g., currency codes must match ISO 4217)
  • Length limits for strings to prevent buffer overflow attacks
public class PricingRequest {
    @NotNull
    @Min(0)
    private BigDecimal basePrice;
    
    @NotNull
    @Pattern(regexp = "^[A-Z]{3}$")
    private String currencyCode;
    
    @ValidDiscounts
    private List<Discount> discounts;
}

SQL Injection Prevention

All database queries are constructed using parameterized prepared statements with ORM (Hibernate) to prevent SQL injection. Raw SQL is never used for user inputs. Stored procedures are avoided in favor of parameterized queries.

Authentication Middleware Design

API authentication uses JWT tokens with HS256 signing. Authentication middleware intercepts all requests and validates:

  • Token signature authenticity
  • Expiration timestamp (5-minute grace period for clock drift)
  • Audience and issuer claims matching service configuration
  • Token revocation status via Redis cache lookup

Authorization Enforcement

Role-based access control (RBAC) is implemented at service layer using Spring Security annotations:

@PreAuthorize("hasRole('PRICING_ADMIN') or @pricingAuthorizationService.canAccessPricing(request)")
public PricingResult calculatePricing(PricingRequest request) {
    // Implementation
}

Sensitive Data Encryption at Rest and in Transit

  • At rest: All pricing data encrypted using AES-256-GCM with keys rotated quarterly via HashiCorp Vault
  • In transit: All API communications secured using TLS 1.3 with HSTS header enforcement
  • Sensitive fields: Currency conversion rates and discount configurations are encrypted at rest

PII Handling and Masking Strategy

No personally identifiable information is stored in pricing calculation context. All user identifiers are hashed using SHA-256 with salt before logging or caching.

Secrets Management

Configuration secrets (API keys, encryption keys) are managed via AWS Secrets Manager with automatic rotation every 90 days. Service accounts use IAM roles with least-privilege access to secrets.

Audit Logging Design

All pricing calculation operations are logged with:

{
  "timestamp": "2023-10-15T14:30:00Z",
  "operation": "PRICING_CALCULATION",
  "user_id": "hashed-user-id",
  "request_id": "uuid-12345",
  "input_params": {
    "base_price": "100.00",
    "currency": "USD"
  },
  "result": {
    "final_price": "95.00",
    "discounts_applied": ["10%_off"]
  },
  "ip_address": "192.168.1.100",
  "user_agent": "API_CLIENT/1.0"
}

Logging follows structured format with trace_id and span_id for distributed tracing correlation. Logs are retained for 90 days with automatic archival to S3 for compliance.

Security Headers Implementation

All HTTP responses include security headers:

  • Content-Security-Policy: "default-src 'self'"
  • X-Content-Type-Options: "nosniff"
  • X-Frame-Options: "DENY"
  • X-XSS-Protection: "1; mode=block"
  • Strict-Transport-Security: "max-age=31536000; includeSubDomains"

Vulnerability Scanning Strategy

  • Static code analysis using SonarQube for security rules
  • Dependency scanning with OWASP Dependency Check
  • Regular penetration testing of API endpoints
  • Automated security scanning in CI pipeline for critical paths

12. Testing Strategy

Unit Testing Scope and Coverage Targets (%)

  • Service Layer: 85% code coverage with focus on business logic, edge cases, and error conditions
  • Domain Objects: 90% coverage for value objects, validation logic, and calculation methods
  • Repository Layer: 70% coverage for core query paths and data access patterns
  • Event Handlers: 80% coverage for async processing logic and error handling
  • API Layer: 65% coverage for request/response validation, middleware logic, and error responses

Integration Testing Strategy

  • Database Integration: Use Testcontainers with PostgreSQL 13+ to test actual database interactions including transactions, constraints, and indexing behavior
  • External Service Mocking: Implement WireMock for third-party pricing engine integrations, with configurable response scenarios (success, timeout, error)
  • Event Broker Testing: Use embedded Kafka or RabbitMQ containers to validate event publishing and consumption flows
  • End-to-End Flow: Test complete pricing calculation workflow from API request to database persistence and event publishing

Contract Testing

  • Pact Broker Integration: Implement consumer-driven contract testing using Pact framework for all external API integrations (e.g., currency conversion, tax calculation services)
  • Versioned Contract Validation: Maintain contract versions with semantic versioning to ensure backward compatibility
  • Contract Evolution Strategy: Define clear process for contract changes with rollback capability and consumer notification

Performance Testing Tool and Key Scenarios

  • Tool: k6 (JavaScript-based load testing) for high-fidelity performance simulation
  • Key Scenarios:
    • Concurrent pricing calculation requests (1000+ concurrent users)
    • High-volume batch price updates (1000+ items in single request)
    • Pricing engine timeout edge case testing (500ms, 1s, 2s timeouts)
    • Memory leak detection under sustained load (24-hour stress test)
  • Metrics Tracked:
    • p50/p95/p99 latency under load (target: <100ms for 95% of requests)
    • Throughput (target: 5000 RPS for single pricing calculation endpoint)
    • Error rate (target: <0.1% under load)

Test Data Management Strategy

  • Fixtures: Maintain JSON fixtures for common pricing scenarios (regular, discounted, tax-inclusive, international)
  • Factories: Implement Factory pattern for generating test data with realistic price ranges and valid combinations
  • Seeding Strategy: Use database migration scripts for initial test data setup with deterministic values for reproducible tests
  • Data Isolation: Use database transactional isolation in unit tests with proper cleanup between test runs
  • Test Data Generation: Implement random data generators for stress testing and boundary condition validation
// Example test class structure for pricing calculator service
public class PricingCalculatorServiceTest {
    
    @Mock
    private PriceRepository priceRepository;
    
    @Mock
    private PricingEngineClient pricingEngine;
    
    @InjectMocks
    private PricingCalculatorService service;
    
    @Test
    public void calculatePrice_WithValidInput_ReturnsCorrectPrice() {
        // Given
        PriceRequest request = createValidRequest();
        PriceResponse expected = createExpectedResponse();
        
        // When
        PriceResponse actual = service.calculatePrice(request);
        
        // Then
        assertEquals(expected, actual);
    }
    
    @Test
    public void calculatePrice_WithInvalidInput_ThrowsValidationException() {
        // Given
        PriceRequest request = createInvalidRequest();
        
        // When & Then
        assertThrows(ValidationException.class, () -> service.calculatePrice(request));
    }
    
    @Test
    public void calculatePrice_WithExternalServiceTimeout_RetriesAndFails() {
        // Given
        when(pricingEngine.calculatePrice(any())).thenThrow(new TimeoutException());
        
        // When & Then
        assertThrows(PricingCalculationException.class, () -> service.calculatePrice(request));
    }
}

13. Observability Design

Structured Logging Schema

All logs must follow a consistent schema to enable effective search, filtering, and analysis:

{
  "timestamp": "2023-10-27T14:30:45.123Z",
  "level": "INFO|WARN|ERROR",
  "service": "pricing-service",
  "trace_id": "12345678-1234-1234-1234-123456789012",
  "span_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "user_id": "user_12345",
  "entity_id": "product_98765",
  "request_id": "req_abcde12345",
  "method": "calculatePrice",
  "message": "Price calculation completed successfully",
  "duration_ms": 45,
  "error_code": "PRICE_CALCULATION_FAILED",
  "error_message": "Invalid product ID provided",
  "stack_trace": "...",
  "payload": {
    "product_id": "prod_12345",
    "quantity": 2,
    "currency": "USD"
  }
}

PII masking rules:

  • User IDs and session tokens are masked in logs
  • Credit card numbers and sensitive fields are redacted
  • Email addresses are truncated to domain-only format

Distributed Tracing Instrumentation

Using OpenTelemetry for distributed tracing with custom spans:

Key Operations to Trace:

  1. calculatePrice - Main price calculation operation
  2. validateProduct - Product validation step
  3. applyDiscounts - Discount application logic
  4. fetchTaxRate - Tax rate retrieval from external service

Span Attributes Captured:

  • product.id: Product identifier
  • currency.code: Currency code used for calculation
  • discount.applied: List of discount codes applied
  • tax.rate: Tax rate percentage
  • request.duration.ms: End-to-end request duration
// Example OpenTelemetry instrumentation in price calculation service
public PriceResult calculatePrice(PriceRequest request) {
    Span span = tracer.spanBuilder("calculatePrice")
        .setAttribute("product.id", request.getProductId())
        .setAttribute("currency.code", request.getCurrency().getCode())
        .startSpan();
    
    try (Scope scope = span.makeCurrent()) {
        // Business logic here
        return processPriceCalculation(request);
    } catch (Exception e) {
        span.recordException(e);
        span.setStatus(StatusCode.ERROR);
        throw e;
    } finally {
        span.end();
    }
}

Metrics Design

RED Method Metrics:

Metric NameTypeLabelsDescription
pricing_requests_totalCountermethod, status, currencyTotal price calculation requests
pricing_errors_totalCountermethod, error_type, currencyPrice calculation errors
pricing_duration_secondsHistogrammethod, currencyPrice calculation latency distribution
active_calculationsGauge-Currently active price calculations

Key Labels:

  • method: calculatePrice, validateProduct, applyDiscounts
  • status: success, error, timeout
  • currency: USD, EUR, GBP
  • error_type: VALIDATION_ERROR, SERVICE_UNAVAILABLE, etc.

Health Check Endpoint Design

Endpoints:

  1. /health - Basic service health (liveness)
  2. /ready - Service readiness (readiness)
  3. /live - Process health
  4. /startup - Startup completion status

Response Format:

{
  "status": "healthy|unhealthy",
  "checks": {
    "database": {
      "status": "healthy",
      "details": "Connected to primary DB"
    },
    "cache": {
      "status": "healthy",
      "details": "Redis cluster connected"
    },
    "external_services": {
      "status": "healthy",
      "details": "All external services reachable"
    }
  },
  "timestamp": "2023-10-27T14:30:45.123Z"
}

Alerting Thresholds

Critical Alerts (Escalation to On-Call):

  1. pricing_errors_total - Rate > 50 errors/minute for 5 minutes
  2. pricing_duration_seconds - p99 > 500ms for 10 minutes
  3. database_connection_failures - > 5 failed connections in 1 minute

Warning Alerts:

  1. pricing_errors_total - Rate > 10 errors/minute for 2 minutes
  2. pricing_duration_seconds - p99 > 200ms for 5 minutes
  3. cache_miss_rate - > 30% cache misses over 15 minutes

On-Call Escalation Path:

  1. Primary engineer (5 min)
  2. Secondary engineer (10 min)
  3. Team lead (15 min)
  4. Engineering manager (30 min)

14. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering alignment

  • Pricing rule complexity vs. performance trade-off: Should we support highly complex pricing rules with extensive conditional logic, or simplify to a more performant but limited rule engine? Complex rules may require full-fledged DSL evaluation engines vs. simple configuration-driven approaches.

  • Real-time vs. batch pricing calculation: Should all price calculations happen in real-time, or can some be pre-computed and cached? Real-time calculation provides accuracy but increases latency; batch calculation reduces load but introduces staleness.

  • Currency handling strategy: Should we support multi-currency pricing natively or delegate currency conversion to external services? Native support increases complexity but provides better control and consistency.

Known trade-offs and justifications

Trade-offOption AOption BJustification
Rule Engine ComplexityFull DSL engine with complex conditionalsSimple configuration-driven rulesDSL provides flexibility but increases maintenance cost and performance overhead. Simple rules are easier to debug and maintain.
Pricing Calculation ScopeReal-time calculation for all requestsPre-computed and cached valuesReal-time ensures accuracy but increases latency; pre-calculation reduces load but introduces staleness risk.
Caching StrategyCache all calculated pricesCache only frequently accessed pricesComprehensive caching improves performance but increases memory usage and cache invalidation complexity.
Concurrency ModelOptimistic locking with retriesPessimistic locking with exclusive accessOptimistic locking provides better throughput but requires retry logic; pessimistic locking ensures consistency but reduces concurrency.

Risks and mitigation strategies

  • Pricing Calculation Inconsistency: Risk of different calculation results for same inputs due to concurrent updates or stale cache. Mitigation: Implement consistent hashing for cache keys, use versioned pricing rules, and enforce strict transaction boundaries.

  • Performance Degradation: Complex pricing calculations can cause latency spikes during peak load. Mitigation: Implement circuit breaker patterns, rate limiting on complex calculations, and pre-computed price caching for popular products.

  • Cache Invalidation Storm: Simultaneous cache invalidation can cause cache misses and performance degradation. Mitigation: Use probabilistic early expiry, distributed locks for cache invalidation, and staggered cache refresh intervals.

  • Currency Conversion Risk: External currency services may fail or provide inconsistent rates. Mitigation: Implement fallback to last known rates, circuit breaker pattern for external services, and regular rate validation.

Technical debt acknowledged

  • Rule Engine Extensibility: Current rule engine design may not scale well for future complex pricing scenarios. Technical debt in maintaining and extending rule evaluation logic.

  • Hardcoded Price Calculation Logic: Some calculation logic is currently hardcoded rather than fully configurable, creating maintenance overhead for future rule changes.

  • Limited Audit Trail: Current implementation has minimal audit trail for pricing changes, which could complicate debugging and compliance in production environments.

  • Testing Coverage Gap: While unit tests cover core logic, end-to-end integration testing with actual pricing rule configurations is limited, creating risk of configuration drift between test and production environments.

Was this article helpful?

Topics in this article

More from Backend LLD

  • Backend LLD

    Recommendation Engine Interface Design

    Comprehensive low-level design for a scalable recommendation engine interface with real-time processing and personalized user experiences.

  • 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.

  • Frontend HLD

    API Communication Architecture Design Patterns

    Comprehensive guide to designing scalable and secure API communication architectures for modern web applications.

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.