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
-
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.
-
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)?
-
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?
-
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.
-
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.
-
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)?
-
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
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GetPrice | /pricing.v1.PricingService/GetPrice | Calculate price for a product with optional discounts and taxes | JWT | 1000 req/hour |
| GetPriceRange | /pricing.v1.PricingService/GetPriceRange | Calculate min/max price range for a product variant | JWT | 500 req/hour |
| ApplyDiscounts | /pricing.v1.PricingService/ApplyDiscounts | Apply multiple discounts to a price | JWT | 2000 req/hour |
| ValidatePriceRules | /pricing.v1.PricingService/ValidatePriceRules | Validate price rule configurations | Admin JWT | 100 req/hour |
| BulkCalculatePrices | /pricing.v1.PricingService/BulkCalculatePrices | Calculate prices for multiple products in batch | JWT | 500 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 Code | Description |
|---|---|
| 200 OK | Successful price calculation |
| 400 Bad Request | Invalid request parameters |
| 401 Unauthorized | Missing or invalid authentication |
| 403 Forbidden | Insufficient permissions |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Server-side error |
| 503 Service Unavailable | System 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 versionpricing.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_idandtimestamp - Supports efficient large result set traversal
- Handles concurrent data changes gracefully
Authentication and Authorization
Authentication: JWT Bearer tokens with custom claims:
user_id: Unique identifiercustomer_segment: User's customer segment for pricing rulesroles: List of user roles (admin, member)
Authorization:
- All endpoints require valid JWT with
user_id - Admin endpoints (
ValidatePriceRules) requireadminrole - 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-Keyheader (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 Name | Fields | Type | Purpose |
|---|---|---|---|
| idx_products_sku | sku | Unique | Fast lookup by SKU |
| idx_price_tiers_name | name | Unique | Fast lookup by tier name |
| idx_price_rules_tier_id | price_tier_id | Standard | Fast rule lookup by tier |
| idx_promotions_active_time | is_active, start_time, end_time | Partial | Fast active promotion lookup |
| idx_product_prices_product_tier | product_id, price_tier_id | Composite | Fast price lookup by product and tier |
| idx_product_prices_effective | effective_from, effective_to | Partial | Fast price validity lookup |
| idx_promotion_rules_promotion_id | promotion_id | Standard | Fast 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_pricesby 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:
- PricingEngine - Main entry point for price calculation
- PriceCalculator - Core business logic orchestrator
- RuleEngine - Rule evaluation and matching engine
- PricingStrategy - Strategy pattern implementations for different pricing models
- 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:
- Input validation
- Rule loading from database
- Pricing strategy application
- Action evaluation and application
- Final price calculation
- 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:
- Immutable pricing objects - all pricing entities are immutable to prevent concurrent modification
- Read-only operations - pricing calculations do not mutate state, only read from immutable sources
- Transaction isolation - database transactions ensure consistency during price updates
- 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:
- Lock ordering: All pricing engine locks are acquired in a consistent order (e.g., by rule ID, then price ID)
- Timeout configuration: 5-second timeout for acquiring database locks to prevent indefinite blocking
- Lock hierarchy: Pricing rule locks are acquired before price calculation locks in all scenarios
- Deadlock detection: Database-level deadlock detection with automatic retry logic
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency is implemented using:
- Idempotency keys in pricing calculation requests
- Cache-based deduplication: Concurrent requests with same key are deduplicated using Redis or in-memory cache
- 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 Type | TTL | Reason |
|---|---|---|
| Product Pricing | 5 minutes | Prices change frequently, need to refresh regularly |
| Price Rules | 1 hour | Rules are less volatile, can be cached longer |
| Promotions | 15 minutes | Promotional pricing is time-sensitive |
| User-specific Pricing | 30 seconds | Personalized 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:
- Probabilistic early expiry: Set TTL to 80% of actual TTL (e.g., 4 minutes for 5-minute cache)
- Distributed locks: Use Redis distributed locks when cache misses occur
- 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:
- When price rules change → invalidate all related pricing entries
- When promotions update → invalidate promotion-specific pricing
- 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:
- Performance: Raw queries allow fine-tuning of database access patterns for complex pricing calculations
- Control: Direct control over query execution plans and index usage
- 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:
- Price calculation updates: When bulk pricing rules change or new pricing tiers are added, these should be processed asynchronously to avoid blocking user requests.
- Historical price archiving: Price history data older than 3 years should be archived to a cold storage system.
- 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:
- Retry logic: Maximum 3 retries with exponential backoff (5s, 25s, 125s)
- DLQ: Messages that fail after max retries are moved to a
pricing.dlqtopic - Manual review: DLQ messages are logged with full context and require manual intervention
- 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:
- Create
OutboxEventrecord in DB - Publish to message broker
- Update
statustoPROCESSEDonly 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
- Domain-level errors: Caught and wrapped in appropriate business exceptions before being propagated up to the API layer
- Infrastructure errors: Bubble up as-is to allow for circuit breaker and retry logic at higher levels
- 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
| Metric | Warning Level | Critical Level | Alerting Channel |
|---|---|---|---|
| Pricing calculation errors | > 10 per minute | > 50 per minute | On-call |
| External service timeouts | > 5 per minute | > 20 per minute | Slack |
| Circuit breaker trips | > 1 per hour | > 5 per hour | PagerDuty |
| Cache misses | > 100 per minute | > 500 per minute |
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:
calculatePrice- Main pricing calculation operationapplyDiscounts- Discount application logicfetchProduct- Product data retrievalvalidatePricingRules- 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 spanPricingEngine.fetchProductData- Database query spanPricingEngine.applyRuleSet- Rule processing sub-spanPricingEngine.publishPriceEvent- Event publishing span
Metrics Design
RED Method Metrics:
| Metric Name | Type | Labels | Description |
|---|---|---|---|
pricing_calculations_total | Counter | operation, status, error_code | Total pricing calculations |
pricing_calculation_duration_seconds | Histogram | operation, status | Latency distribution |
pricing_errors_total | Counter | operation, error_type, error_code | Error counts by type |
pricing_cache_hits_total | Counter | cache_type, hit_status | Cache hit/miss statistics |
pricing_rules_loaded_total | Gauge | rule_type, version | Active pricing rules count |
Label Design:
operation: calculatePrice, fetchProduct, applyDiscountsstatus: success, failure, timeouterror_type: validation, internal, externalerror_code: VALIDATION_ERROR, RULE_NOT_FOUND, DB_TIMEOUTcache_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_secondsp99 > 500ms (warn: 200ms, critical: 500ms)pricing_errors_totalrate > 10/minute (warn: 5/minute, critical: 10/minute)pricing_cache_hits_totalratio < 80% (warn: 85%, critical: 80%)
Warning Alerts:
pricing_calculation_duration_secondsp95 > 200ms (warn: 100ms, critical: 200ms)pricing_errors_totalrate > 2/minute (warn: 1/minute, critical: 2/minute)pricing_cache_hits_totalratio < 90% (warn: 95%, critical: 90%)
On-call Escalation Path:
- First alert → Slack notification to on-call engineer
- Second alert within 5 minutes → Phone call to primary on-call
- Third alert within 10 minutes → Escalate to engineering manager
Service-Level Observability
Key Metrics Dashboard:
- Pricing calculation success rate (99.9% target)
- Latency distribution (p50/p95/p99 < 200ms)
- Cache hit ratio (target > 95%)
- Error rate (target < 0.1%)
- 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-off | Option 1 | Option 2 | Justification |
|---|---|---|---|
| Consistency vs. Availability | Strong consistency with distributed transactions | Eventual consistency with eventual price updates | For 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. Performance | Complex rule engine with extensive conditionals | Simple tiered pricing rules | Complex 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 ratio | Short TTLs (30 seconds) for high accuracy | Long TTLs (5 minutes) for better cache hit ratio | Short 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. microservices | Single pricing engine service | Separate 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
| Risk | Impact | Mitigation |
|---|---|---|
| Price calculation inconsistency | Customer sees different prices for same item across sessions or devices | Implement comprehensive test suite with price validation scenarios, use deterministic rule execution, and maintain detailed audit logs |
| High latency under load | Pricing requests timeout or cause poor user experience | Implement aggressive caching, use read replicas for database queries, and design price calculation with minimal external dependencies |
| Currency conversion errors | Incorrect pricing due to exchange rate fluctuations or calculation errors | Use reliable external currency services with fallback mechanisms, implement currency validation checks, and maintain historical exchange rate data |
| Race conditions in concurrent price updates | Inconsistent pricing due to simultaneous rule modifications | Implement optimistic locking on pricing rules, use distributed locks for critical updates, and design idempotent update operations |
| Cache stampede | Cache misses causing database overload | Implement probabilistic early expiry, use distributed locks for cache population, and set up proper cache warming strategies |
| Rule engine performance degradation | Slow price calculation as rule complexity increases | Set 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