Most software systems are built around a simple assumption:
The price is already known.
A product has a price.
A ticket has a price.
A hotel room has a price.
A delivery has a price.
The application simply retrieves that number from a database and shows it to the customer.
But what if the price itself were part of the computation?
What if the system could observe demand, inventory, customer behavior, time, competition, and historical transactions — and continuously adjust prices based on what it learns?
Now we are no longer building a CRUD application.
We are building a dynamic pricing engine.
And dynamic pricing is much more interesting than:
price = 100
It becomes:
price = f(
demand,
inventory,
time,
conversion_rate,
historical_sales,
seasonality,
competition,
business_rules
)
The price becomes an output of a system.
That system observes the world.
The world changes.
The system learns.
And the price changes with it.
This article walks through how to design such an engine from scratch.
1. The Core Idea
Imagine an online store selling a product.
Suppose the product costs:
$100
At 9:00 AM:
Demand = low
Inventory = high
Conversion = 2%
The engine might keep the price near:
$100
But at 6:00 PM:
Demand = extremely high
Inventory = low
Conversion = 9%
The engine might calculate:
$118
Later, demand collapses:
Demand = low
Inventory = high
Conversion = 1%
The price could move toward:
$94
The important part is that these numbers are not manually configured one by one.
The system is responding to observed conditions.
Conceptually:
┌──────────────┐
│ Customers │
└──────┬───────┘
│
▼
┌──────────────┐
│ Event Stream │
└──────┬───────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Demand Inventory Conversion
Signals Signals Signals
│ │ │
└────────────────┼────────────────┘
▼
┌─────────────────┐
│ Pricing Engine │
└────────┬────────┘
│
▼
New Price
│
▼
Customer
This is the architecture we want.
2. Why Dynamic Pricing Is Difficult
At first glance, dynamic pricing looks like a mathematical problem.
It isn't only mathematical.
It is simultaneously:
- a data problem
- a distributed systems problem
- a machine learning problem
- an optimization problem
- a financial system
- a consistency problem
- a product design problem
Consider a simple pricing formula:
price = base_price * demand_multiplier
That looks easy.
But where does demand_multiplier come from?
Suppose 10,000 customers are browsing simultaneously.
Some are purchasing.
Some are abandoning carts.
Some are repeatedly refreshing the product page.
Some are buying multiple units.
Inventory is decreasing.
Competitors are changing their prices.
The time of day is changing.
A promotion has started.
Your model has not seen this exact situation before.
Now the problem becomes much more interesting.
3. Define the Pricing Objective
Before writing the algorithm, define what the engine is trying to optimize.
Different businesses have different objectives.
For example:
maximize revenue
or:
maximize profit
or:
maximize inventory utilization
or:
maximize conversion
or:
balance revenue and customer retention
A pricing engine without an objective is just a number generator.
Let's define a simple objective:
maximize expected profit
Expected profit can be represented as:
Expected Profit =
(price - variable_cost)
× expected_quantity_sold
But there is a catch.
Increasing price can reduce quantity sold.
Therefore:
profit(price)
is not necessarily increasing with price.
For example:
Price Expected Sales Revenue
$80 100 $8,000
$100 90 $9,000
$120 65 $7,800
$140 40 $5,600
The engine needs to find the region where the business objective is strongest.
That is an optimization problem.
4. Build the Data Pipeline First
The pricing algorithm is only as good as the signals feeding it.
We need events.
For example:
{
"event": "product_view",
"product_id": "P100",
"user_id": "U500",
"timestamp": "2026-09-18T18:30:00Z"
}
Purchase:
{
"event": "purchase",
"product_id": "P100",
"quantity": 2,
"price": 110,
"timestamp": "2026-09-18T18:31:12Z"
}
Cart abandonment:
{
"event": "cart_abandoned",
"product_id": "P100",
"timestamp": "2026-09-18T18:35:00Z"
}
Inventory:
{
"product_id": "P100",
"inventory": 37,
"timestamp": "2026-09-18T18:40:00Z"
}
The system can consume these events through a stream.
Conceptually:
Application
│
▼
Event Collector
│
▼
Message Broker
│
├───────────────┐
▼ ▼
Analytics Pricing Engine
│ │
▼ ▼
Data Lake Price Store
Technologies could include Kafka, Redis Streams, NATS, PostgreSQL, ClickHouse, or other systems depending on scale.
The technology is secondary.
The architecture is the important part.
5. Turn Raw Events Into Demand Signals
Raw events are not directly useful for pricing.
We need features.
Suppose the last hour produced:
views = 12,000
carts = 1,200
purchases = 360
inventory = 80
We can derive:
cart_rate = carts / views
Therefore:
cart_rate = 10%
And:
conversion_rate = purchases / views
Which gives:
conversion_rate = 3%
We could also calculate:
sales_velocity =
units_sold / time_window
For example:
sales_velocity = 40 units/hour
With 80 units remaining:
estimated_hours_to_stockout = 80 / 40
Therefore:
2 hours
That is an extremely valuable signal.
The engine can now reason:
Demand is high and the product may sell out within two hours.
6. Demand Is a Time Series
Demand is rarely constant.
Imagine:
Hour Sales
08:00 10
09:00 13
10:00 15
11:00 17
12:00 24
13:00 29
14:00 34
15:00 31
16:00 27
17:00 41
The engine should understand trends.
A simple moving average is a useful starting point.
def moving_average(values, window):
if len(values) < window:
return sum(values) / len(values)
return sum(values[-window:]) / window
For example:
recent_sales = [24, 29, 34, 31, 27]
demand = moving_average(recent_sales, 5)
This smooths noisy observations.
But smoothing creates another problem.
You can become too slow to react.
If demand suddenly explodes, a long moving average may hide the spike.
That is where exponential weighting becomes useful.
7. Exponentially Weighted Demand
We can update demand continuously:
new_demand = (
alpha * current_sales
+ (1 - alpha) * previous_demand
)
Where:
0 < alpha <= 1
A larger alpha means the system reacts faster.
For example:
alpha = 0.7
makes the engine highly responsive.
While:
alpha = 0.1
makes it much more stable.
This creates a fundamental engineering tradeoff:
responsiveness
vs
stability
A pricing engine that reacts too quickly can oscillate.
A pricing engine that reacts too slowly can miss opportunities.
8. Introduce Inventory Pressure
Demand alone is insufficient.
Suppose two products both receive:
100 purchases/hour
Product A has:
10,000 units
Product B has:
100 units
Their pricing situations are completely different.
We can define an inventory pressure signal:
inventory_pressure =
1 - (inventory / target_inventory)
If inventory is below target, pressure increases.
For example:
inventory = 200
target = 1000
Then:
inventory_pressure = 0.8
The engine can interpret this as:
high inventory pressure
We can combine it with demand:
pressure =
demand_score * inventory_pressure
Now the pricing engine understands not just how many people want the product, but how urgently the business needs to respond.
9. Build a Pricing Function
Let's start with a simple model.
def calculate_price(
base_price,
demand_score,
inventory_pressure,
conversion_score
):
multiplier = (
1
+ 0.20 * demand_score
+ 0.15 * inventory_pressure
+ 0.10 * conversion_score
)
return base_price * multiplier
This is not a production pricing model.
It is a starting point.
Suppose:
base_price = $100
demand_score = 0.8
inventory_pressure = 0.7
conversion_score = 0.6
Then:
multiplier =
1
+ 0.20(0.8)
+ 0.15(0.7)
+ 0.10(0.6)
Approximately:
1.35
Price:
$135
Now we have a functioning dynamic pricing mechanism.
But there is a dangerous problem.
10. Never Let the Model Control Everything
Machine learning systems can make surprising decisions.
Pricing is especially sensitive because mistakes directly affect money.
Therefore, we need guardrails.
For example:
MIN_MULTIPLIER = 0.80
MAX_MULTIPLIER = 1.30
Then:
multiplier = max(
MIN_MULTIPLIER,
min(multiplier, MAX_MULTIPLIER)
)
The engine can never move beyond the configured boundary.
We can also impose:
maximum change per hour
minimum margin
minimum price
maximum price
inventory constraints
promotion constraints
legal constraints
The final architecture becomes:
Signals
│
▼
Model
│
▼
Candidate Price
│
▼
Business Rules
│
▼
Risk Controls
│
▼
Final Price
The model proposes.
The policy layer decides whether the proposal is allowed.
That separation is extremely important.
11. Add Price Elasticity
Now we reach the more interesting part.
Customers respond to price.
If we increase the price, demand may decrease.
This relationship is called price elasticity of demand.
A simplified formulation is:
elasticity =
% change in quantity
--------------------
% change in price
Suppose:
price increases 10%
sales decrease 5%
Then elasticity is approximately:
-0.5
The negative sign represents the typical inverse relationship between price and demand.
A pricing engine can learn this relationship from historical observations.
Suppose we store:
price
quantity
timestamp
inventory
promotion
traffic
We can estimate how quantity changes as price changes.
The system can then ask:
If I set the price to $110 instead of $100, how many units do I expect to sell?
That question is much more powerful than simply saying:
Demand is high.
12. Learn From Experiments
Here is where the system starts becoming genuinely adaptive.
Suppose the engine always charges:
$100
It will have a huge amount of data about demand at $100.
But almost no information about:
$90
$95
$105
$110
$120
This creates a problem.
The model cannot learn relationships it never observes.
Therefore, the pricing system needs controlled experimentation.
For example:
90% traffic → current optimal price
10% traffic → experimental price
Suppose:
Control price = $100
Experimental price = $110
We observe:
Control conversion = 5.0%
Experiment conversion = 4.6%
But revenue per visitor becomes:
control:
100 × 0.05 = $5.00
experiment:
110 × 0.046 = $5.06
The experiment suggests that the higher price might produce more revenue per visitor.
Now the engine has learned something.
13. Exploration vs Exploitation
This creates one of the classic problems in intelligent systems:
exploration vs exploitation.
Exploitation means:
Use what we currently believe is best.
Exploration means:
Try something uncertain so we can learn.
A pricing engine needs both.
If we only exploit:
use current best price forever
the model stops learning.
If we explore too aggressively:
randomly change prices constantly
the system becomes unstable and potentially harmful to the business.
A simple strategy is:
95% exploitation
5% exploration
More advanced systems can use:
- multi-armed bandits
- Thompson sampling
- Bayesian optimization
- contextual bandits
- reinforcement learning
The key insight is simple:
A learning system needs opportunities to learn.
14. Contextual Pricing
Price does not exist in isolation.
The same product can behave differently depending on context.
For example:
Monday morning
Friday evening
holiday
rainy day
payday
end of month
Demand can change.
We can represent context as features:
features = {
"hour": 18,
"day_of_week": 5,
"month": 9,
"inventory": 83,
"sales_velocity": 42,
"conversion_rate": 0.07,
"traffic": 12000
}
The model then estimates:
expected_demand(price, context)
Now pricing becomes a prediction problem.
15. The Architecture
A more complete system could look like this:
┌───────────────────┐
│ Web / Mobile Apps │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Event Gateway │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Event Stream │
└─────────┬─────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Demand Service Inventory Service Analytics
│ │ │
└────────────────┼────────────────┘
▼
┌───────────────────┐
│ Feature Store │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Pricing Model │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Policy / Guardrail│
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Price Calculator │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Price Cache │
└─────────┬─────────┘
│
▼
Customers
There should also be a feedback loop:
Customers
│
▼
Transactions
│
▼
Events
│
▼
Features
│
▼
Model
│
▼
New Prices
│
└──────────────► Customers
This is the heart of the system.
16. Separate Pricing From Checkout
This is an important architectural decision.
The pricing service should not be tightly coupled to checkout.
Instead:
GET /products/P100/price
could return:
{
"product_id": "P100",
"price": 109.50,
"currency": "USD",
"price_version": "pv_87231",
"expires_at": "2026-09-18T19:05:00Z"
}
When checkout begins, the system records the price version.
order
├── product
├── quantity
├── unit_price
├── price_version
└── currency
This protects historical correctness.
If the price changes five seconds later, an existing order should not magically change.
17. Price Versioning
Every generated price should have a version.
For example:
P100
Price: $109.50
Version: 87231
Then:
P100
Price: $112.00
Version: 87232
The system now has an audit trail.
We can answer:
What price did the customer see?
and:
Why was this price generated?
For example:
{
"price": 112.00,
"base_price": 100.00,
"demand_score": 0.81,
"inventory_pressure": 0.72,
"conversion_rate": 0.071,
"model_version": "model_42",
"policy_version": "policy_7",
"timestamp": "2026-09-18T19:02:00Z"
}
This turns the pricing engine into an observable system rather than a black box.
18. The Price Cache
Pricing is often requested far more frequently than prices need to be recalculated.
If 100,000 customers request the same product price, we should not run the model 100,000 times.
Instead:
Customer
│
▼
API
│
▼
Redis
│
├── hit → return price
│
└── miss
│
▼
Pricing Engine
For example:
price:P100
could contain:
{
"price": 109.50,
"version": 87231,
"expires_at": 1758222300
}
The TTL depends on how quickly the market changes.
For some businesses:
5 minutes
might be reasonable.
For others:
1 hour
could be enough.
The point is to separate price computation frequency from price retrieval frequency.
19. Avoid Price Oscillation
Imagine this:
Price = $100
Demand rises
Price = $110
Demand falls
Price = $98
Demand rises
Price = $112
Demand falls
Price = $97
The system is oscillating.
Customers may see unstable pricing.
The business may also struggle to reason about revenue.
A damping mechanism can help.
Instead of immediately moving to:
new_price = $120
we can move gradually:
new_price = (
0.8 * old_price
+ 0.2 * candidate_price
)
If:
old_price = 100
candidate_price = 120
then:
new_price = 104
The engine moves toward the new equilibrium rather than jumping instantly.
20. Add Hysteresis
Another useful technique is hysteresis.
Suppose the engine only changes the price when the predicted improvement is significant.
For example:
if abs(candidate_price - current_price) < threshold:
keep_current_price()
If the threshold is:
$3
then:
$100 → $101
does nothing.
But:
$100 → $108
triggers a change.
This prevents tiny fluctuations from constantly generating new prices.
21. Build the Learning Loop
A complete adaptive engine can follow this cycle:
1. Observe
2. Aggregate
3. Predict
4. Optimize
5. Validate
6. Publish
7. Measure
8. Learn
9. Repeat
In pseudocode:
while True:
events = collect_recent_events()
features = build_features(events)
demand = predict_demand(features)
candidate_prices = generate_candidates(
base_price=features["base_price"]
)
best_price = optimize(
candidate_prices,
demand,
features
)
safe_price = apply_guardrails(
best_price,
features
)
publish_price(safe_price)
sleep(interval)
The system is effectively creating a feedback controller.
22. Make the Model Replaceable
Do not hard-code the pricing algorithm everywhere.
Create an interface:
class PricingModel:
def predict(self, features):
raise NotImplementedError
Then:
class RuleBasedModel(PricingModel):
def predict(self, features):
...
Later:
class RegressionModel(PricingModel):
def predict(self, features):
...
Later:
class BanditModel(PricingModel):
def predict(self, features):
...
The pricing infrastructure remains stable while the intelligence evolves.
This is a powerful architecture pattern.
The system should not care which mathematical brain is currently inside it.
23. Database Design
A simple relational schema could look like:
CREATE TABLE products (
id UUID PRIMARY KEY,
base_price DECIMAL(12,2),
cost DECIMAL(12,2),
inventory INTEGER
);
Price history:
CREATE TABLE price_history (
id BIGSERIAL PRIMARY KEY,
product_id UUID NOT NULL,
price DECIMAL(12,2) NOT NULL,
model_version VARCHAR(100),
demand_score DECIMAL(8,4),
inventory_pressure DECIMAL(8,4),
created_at TIMESTAMP NOT NULL
);
Experiments:
CREATE TABLE pricing_experiments (
id UUID PRIMARY KEY,
product_id UUID NOT NULL,
control_price DECIMAL(12,2),
experiment_price DECIMAL(12,2),
traffic_ratio DECIMAL(5,4),
started_at TIMESTAMP,
ended_at TIMESTAMP
);
Transactions should also preserve the actual unit price:
CREATE TABLE orders (
id UUID PRIMARY KEY,
product_id UUID NOT NULL,
quantity INTEGER NOT NULL,
unit_price DECIMAL(12,2) NOT NULL,
price_version BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL
);
This gives us both operational data and historical evidence.
24. Observability
A pricing engine without observability is dangerous.
We need metrics such as:
price_changes_per_minute
average_price
revenue_per_visitor
conversion_rate
gross_margin
inventory_turnover
model_error
prediction_confidence
experiment_lift
We should also log:
old price
new price
reason
model version
feature values
policy decision
timestamp
For example:
PRICE_CHANGE
product=P100
old=104.00
new=109.50
demand_score=0.82
inventory_pressure=0.71
conversion=0.068
model=model_42
policy=policy_7
When something goes wrong, this becomes invaluable.
25. Failure Modes
Dynamic pricing systems can fail in interesting ways.
Failure 1: Feedback loops
Suppose:
high demand → high price
Then customers buy less.
The system interprets:
lower sales → lower demand
and lowers the price.
Now demand rises again.
The system increases the price again.
You have created an unstable loop.
Failure 2: Bad data
Suppose a tracking bug reports:
10,000 purchases
when only:
100
occurred.
The pricing engine might interpret this as explosive demand.
Bad telemetry becomes bad pricing.
Failure 3: Cold start
A new product has:
zero historical sales
How do we price it?
The system needs defaults.
Possibilities include:
base pricing
category-level demand
similar-product data
manual configuration
conservative exploration
Failure 4: Model drift
Customer behavior changes.
A model trained on last year's behavior may become less accurate this year.
Therefore, models need monitoring and retraining.
26. The Most Important Safety Mechanism: Kill Switch
Every adaptive pricing engine should have a kill switch.
Something like:
pricing_engine.enabled = false
When disabled:
dynamic_price = base_price
This sounds simple.
It is one of the most important features in the entire system.
If the model starts generating abnormal prices, the business should be able to immediately return to a safe pricing policy.
Automation should always have a manual escape hatch.
27. Scaling the System
At small scale:
PostgreSQL
+
Redis
+
Background Worker
may be enough.
At larger scale:
Kafka
+
Stream Processing
+
Feature Store
+
Model Serving
+
Redis
+
OLAP Database
becomes more appropriate.
The architecture might evolve into:
Events
│
▼
Kafka
│
┌────────┴────────┐
▼ ▼
Stream Processor Data Lake
│
▼
Feature Store
│
▼
Model Server
│
▼
Pricing Optimizer
│
▼
Policy Engine
│
▼
Redis Price Cache
│
▼
Applications
The system grows because the workload grows.
The fundamental idea does not change.
28. The Deeper Idea
A dynamic pricing engine is not really about prices.
It is about feedback.
The system observes the environment.
It makes a decision.
The environment reacts.
The system observes that reaction.
Then it makes another decision.
That is a control loop.
Environment
│
▼
Observation
│
▼
Model
│
▼
Decision
│
▼
Action
│
▼
Environment
│
└───────────────►
This same architecture appears in:
- recommendation systems
- robotics
- automated trading
- traffic systems
- cloud autoscaling
- game economies
- energy management
- logistics
- fraud detection
The pricing engine is therefore an excellent project for learning intelligent systems.
It forces you to combine software architecture with mathematics.
29. From Rules to Intelligence
The first version might look like:
if demand > 0.8:
price *= 1.10
That is rules.
The second version might estimate demand:
expected_demand(price, context)
That is prediction.
The third version might optimize price:
argmax price × expected_quantity(price)
That is optimization.
The fourth version might experiment:
try price A
try price B
observe results
update beliefs
That is learning.
The architecture evolves from:
rules
↓
prediction
↓
optimization
↓
experimentation
↓
adaptive decision-making
That progression is where the project becomes genuinely interesting.
30. A Practical Build Roadmap
If I were building this system from scratch, I would not start with reinforcement learning.
I would build it in stages.
Version 1 — Static Pricing
Implement:
products
base prices
inventory
checkout
price history
Version 2 — Rule-Based Pricing
Add:
demand score
inventory pressure
price bounds
Version 3 — Event-Driven Pricing
Add:
event ingestion
stream processing
real-time demand calculation
Redis price cache
Version 4 — Elasticity
Add:
historical price
historical quantity
price elasticity estimation
Version 5 — Prediction
Introduce:
machine learning model
feature engineering
demand forecasting
model versioning
Version 6 — Optimization
Allow the system to evaluate multiple prices:
$95
$100
$105
$110
$115
and estimate expected outcomes.
Version 7 — Experimentation
Introduce:
A/B testing
contextual bandits
controlled exploration
Version 8 — Full Adaptive Engine
Finally:
observe
→ predict
→ optimize
→ experiment
→ measure
→ learn
→ repeat
At this point, you have something far beyond a pricing table.
You have built a decision-making system.
Conclusion
A traditional pricing system answers:
What is the price?
A dynamic pricing engine asks a much more interesting question:
Given everything the system currently knows, what price should exist right now?
That small change in perspective completely changes the architecture.
You need event streams.
You need demand estimation.
You need feature engineering.
You need optimization.
You need experimentation.
You need price versioning.
You need caching.
You need guardrails.
You need observability.
And eventually, you need a learning loop.
The most important lesson is not the formula.
It is the architecture.
A truly adaptive system does not simply execute a fixed set of instructions.
It observes reality.
It turns reality into data.
It turns data into signals.
It turns signals into decisions.
And then it watches what happens next.
That is the architecture of a system that learns.
And once you understand that architecture, dynamic pricing becomes just one example.
You can apply the same thinking to inventory, advertising, logistics, recommendations, cloud infrastructure, fraud detection, and autonomous decision systems.
The interesting part of modern software is increasingly not just building systems that do things.
It is building systems that observe what happened, learn from it, and decide what to do next.
That is where software starts behaving less like a static machine and more like an adaptive organism.
And that is where engineering gets interesting.