Building a Dynamic Pricing Engine That Learns From Demand

Building a Dynamic Pricing Engine That Learns From Demand

Leader 2 2 7
calendar_today agoschedule5 min read

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.

1 Comment

0 votes
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Merancang Backend Bisnis ISP: API Pelanggan, Paket Internet, Invoice, dan Tiket Support

Masbadar - Mar 13

The Zero-Net-Loss Fleet & The Mercenary Squad: A Live AI Economy

DEVPlank - Aug 4

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

Frameworks Are Institutional Memory

Ken W. Algerverified - Sep 17

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23
chevron_left
921 Points11 Badges
8Posts
1Comments
33Connections
Derek Mwale — Where Code Meets Creativity.

Related Jobs

View all jobs →

Commenters (This Week)

9 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!