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
- What is the scope of pricing calculation? Will it handle simple product pricing, complex subscription tiers, or dynamic pricing based on market conditions?
- What is the expected scale? How many concurrent price calculations are expected, and what is the peak volume of pricing requests per second?
- 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)?
- What is the read/write ratio? Is pricing data mostly read (e.g., product listings) or frequently updated (e.g., promotional campaigns)?
- What latency sensitivity is acceptable? Should pricing be calculated in <10ms for real-time display, or can it tolerate up to 500ms?
- What authentication model is in place? Will pricing be exposed to public users or only authenticated service consumers?
- 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
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/pricing-rules | Create new pricing rule | JWT + Admin |
| GET | /v1/pricing-rules | List all pricing rules | JWT + Admin |
| GET | /v1/pricing-rules/{id} | Get specific pricing rule | JWT + Admin |
| PUT | /v1/pricing-rules/{id} | Update pricing rule | JWT + Admin |
| DELETE | /v1/pricing-rules/{id} | Delete pricing rule | JWT + Admin |
| POST | /v1/calculations | Calculate price for item(s) | JWT + User |
| GET | /v1/calculations/{id} | Get calculation result | JWT + User |
| POST | /v1/calculations/preview | Preview calculation without persisting | JWT + 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 Code | Description |
|---|---|
| 200 | Success (GET, PUT, PATCH) |
| 201 | Created (POST) |
| 400 | Bad Request (validation errors, malformed JSON) |
| 401 | Unauthorized (missing or invalid auth) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found (resource doesn't exist) |
| 429 | Too Many Requests (rate limiting) |
| 500 | Internal 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
| Endpoint | Limit | Period | Enforcement |
|---|---|---|---|
/v1/calculations | 1000 requests/hour | Hourly | In-memory + Redis |
/v1/pricing-rules | 100 requests/hour | Hourly | In-memory + Redis |
/v1/calculations/preview | 500 requests/hour | Hourly | In-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:
- Distributed system compatibility - No coordination required between services for ID generation
- No hotspots - Eliminates contention in database clusters
- Scalability - Supports horizontal sharding and cross-service references
- Security - Prevents enumeration attacks by making IDs unpredictable
Index Design Justification
| Index | Fields | Type | Purpose |
|---|---|---|---|
idx_products_category | category | Standard | Fast filtering by product categories |
idx_products_active | is_active | Standard | Efficiently filter active products |
idx_price_rules_priority | priority | Standard | Fast rule selection by priority |
idx_price_rules_active | is_active | Standard | Efficiently filter active rules |
idx_product_prices_product_id | product_id | Standard | Fast lookup of prices by product |
idx_product_prices_active | is_active | Standard | Efficiently filter active price records |
idx_product_prices_effective_date | effective_from, effective_to | Composite | Fast time-range queries for price validity |
idx_price_tiers_price_rule_id | price_rule_id | Standard | Fast 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:
- Auditability: Historical price changes can be traced
- Recovery capability: Data can be restored if accidentally deleted
- Referential integrity: Price rules and tiers remain valid in historical contexts
- 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
- Active price rules: Keep in primary database
- Historical price data: Archive to time-series or data warehouse after 1 year
- Inactive rules: Move to read-only archive database after 2 years
- 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:
- PricingService: Main entry point for price calculation operations
- PriceCalculator: Core business logic engine that orchestrates rule application
- PriceRuleEngine: Manages the execution of pricing rules in proper sequence
- PriceRule: Interface defining rule behavior for extensibility
- 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:
- Consistent state: All pricing rule calculations happen atomically
- Data integrity: Ensures no partial updates occur during calculation
- Isolation level: READ_COMMITTED to prevent dirty reads while maintaining performance
- 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:
- Immutable pricing objects - All price calculation components are immutable to prevent concurrent modification
- Atomic operations for cache updates and state transitions using
java.util.concurrent.atomicclasses - Read-Write locks for shared pricing rule collections to allow concurrent reads while blocking writes
- 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:
- Lock ordering - All pricing rule locks acquired in consistent order (by ID or name)
- Timeout configuration - 500ms timeout for lock acquisition to prevent indefinite waits
- Hierarchical locking - Price rule locks acquired before calculation context locks
- Thread pool size limiting - Controlled concurrent price calculations to prevent resource exhaustion
Idempotency Implementation for Concurrent Duplicate Requests
Idempotency is handled through:
- Idempotency keys in request headers (
X-Idempotency-Key) - Cache-based idempotency - Store calculation results with key for 5 minutes
- 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 Type | TTL | Reason |
|---|---|---|
| Product prices | 1 hour | Prices may change daily but not immediately |
| Currency rates | 30 minutes | Exchange rates update frequently |
| Pricing rules | 24 hours | Rules change less frequently than prices |
| User-specific pricing | 15 minutes | Personalized 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:
- Probabilistic early expiry: Set TTL to 75% of actual TTL (e.g., 45 minutes for 1-hour entries)
- Distributed locks: Use Redis distributed locks when cache misses occur to prevent multiple concurrent recalculations
- 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:
- Write operations: When pricing rules or product data changes, publish invalidation events to Redis channels
- Cache invalidation: Consumers listen for these events and invalidate relevant cache entries
- 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.changedtopic - 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 Type | Retryable | Reason |
|---|---|---|
| ValidationException | No | Client error, no point in retry |
| BusinessLogicException | No | Business rule violation |
| DatabaseException | Yes | Transient DB issues |
| ExternalServiceException | Yes | Network or service availability |
| CacheException | Yes | Cache system issues |
| TimeoutException | Yes | Temporary 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:
calculatePrice- Main price calculation operationvalidateProduct- Product validation stepapplyDiscounts- Discount application logicfetchTaxRate- Tax rate retrieval from external service
Span Attributes Captured:
product.id: Product identifiercurrency.code: Currency code used for calculationdiscount.applied: List of discount codes appliedtax.rate: Tax rate percentagerequest.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 Name | Type | Labels | Description |
|---|---|---|---|
pricing_requests_total | Counter | method, status, currency | Total price calculation requests |
pricing_errors_total | Counter | method, error_type, currency | Price calculation errors |
pricing_duration_seconds | Histogram | method, currency | Price calculation latency distribution |
active_calculations | Gauge | - | Currently active price calculations |
Key Labels:
method: calculatePrice, validateProduct, applyDiscountsstatus: success, error, timeoutcurrency: USD, EUR, GBPerror_type: VALIDATION_ERROR, SERVICE_UNAVAILABLE, etc.
Health Check Endpoint Design
Endpoints:
/health- Basic service health (liveness)/ready- Service readiness (readiness)/live- Process health/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):
pricing_errors_total- Rate > 50 errors/minute for 5 minutespricing_duration_seconds- p99 > 500ms for 10 minutesdatabase_connection_failures- > 5 failed connections in 1 minute
Warning Alerts:
pricing_errors_total- Rate > 10 errors/minute for 2 minutespricing_duration_seconds- p99 > 200ms for 5 minutescache_miss_rate- > 30% cache misses over 15 minutes
On-Call Escalation Path:
- Primary engineer (5 min)
- Secondary engineer (10 min)
- Team lead (15 min)
- 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-off | Option A | Option B | Justification |
|---|---|---|---|
| Rule Engine Complexity | Full DSL engine with complex conditionals | Simple configuration-driven rules | DSL provides flexibility but increases maintenance cost and performance overhead. Simple rules are easier to debug and maintain. |
| Pricing Calculation Scope | Real-time calculation for all requests | Pre-computed and cached values | Real-time ensures accuracy but increases latency; pre-calculation reduces load but introduces staleness risk. |
| Caching Strategy | Cache all calculated prices | Cache only frequently accessed prices | Comprehensive caching improves performance but increases memory usage and cache invalidation complexity. |
| Concurrency Model | Optimistic locking with retries | Pessimistic locking with exclusive access | Optimistic 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.