Skip to main content

Command Palette

Search for a command to run...

Designing Real-Time Pricing, Quotes and Orders on AWS

Updated
14 min readView as Markdown

Real-time pricing in an investment platform looks simple from the customer’s perspective: open a portfolio, view its current value and submit an order.

Behind that apparently simple flow is a difficult distributed-systems problem. Market prices change continuously, messages can arrive out of order, data vendors can fail, and a price shown to a customer may become stale within seconds.

The central design question is:

How can we process continuously changing market prices while guaranteeing that an order uses the exact price accepted by the customer?

The answer begins by separating two fundamentally different types of data.

Two Data Paths with Different Responsibilities

The platform should maintain two independent data paths:

  1. Market-price data — high-volume, continuously changing and optimized for fast reads and writes.

  2. Quote and order data — lower-volume, transactional, immutable and auditable.

The latest market price and an accepted customer price are not the same business fact.

The pricing platform tells us what an instrument is worth now. The quote and ordering platform records what price the business offered and what the customer accepted.

Trying to store both in the same database creates unnecessary coupling. A sudden increase in market-data traffic could then affect order processing, even though orders have much stronger consistency requirements.

AWS-Native Architecture

The architecture can be built using the following components:

  • Vendor adapters running on Amazon EKS or ECS

  • Amazon MSK for durable price-event streaming

  • DynamoDB for the latest price of each instrument

  • Amazon Aurora PostgreSQL for quotes and orders

  • Amazon S3 for historical price events and replay

  • A transactional outbox for reliable business-event publication

  • CloudWatch, OpenTelemetry and Prometheus for observability

The logical flow is:

Primary and fallback vendors
        ↓
Vendor adapters
        ↓
Amazon MSK
        ↓
Price processor
        ↓
DynamoDB latest-price store
        ↓
Portfolio and Quote services
        ↓
Aurora quote and order database

Amazon MSK decouples the external vendors from the internal pricing platform. It absorbs bursts, supports replay and allows price-processing consumers to scale independently.

Events should be partitioned by instrumentId. This preserves ordering for a particular instrument without forcing the entire market-data stream through one partition. However, partitioning by instrument can create hot partitions for high-volume instruments such as AAPL, TSLA or index futures. The deployment should therefore provision enough MSK partitions and consumers for the expected concentration, monitor per-partition throughput and lag, and define an explicit policy for skew. If a single instrument can exceed the capacity of one partition, the design must either allocate that instrument to a dedicated feed or shard its events while preserving ordering through a downstream per-instrument sequencer. The default design accepts one ordered partition per instrument when its throughput remains within the partition limit.

Why DynamoDB for Current Prices?

The latest-price store has a simple but demanding workload:

  • frequent updates;

  • access by instrument identifier;

  • predictable low-latency reads;

  • independent scaling;

  • no requirement for relational joins.

A representative price record might contain:

{
  "instrumentId": "AAPL",
  "bid": 229.12,
  "ask": 229.18,
  "currency": "USD",
  "vendor": "PRIMARY",
  "vendorTimestamp": "2026-08-28T15:30:12.341Z",
  "receivedAt": "2026-08-28T15:30:12.366Z",
  "sourceSequence": 91827364,
  "priceVersion": "PRIMARY:91827364",
  "status": "LIVE"
}

DynamoDB is well suited to this access pattern because the processor can continuously replace the latest state for each instrument.

Historical prices do not need to remain in the same table indefinitely. The original events can be retained in Kafka for a configured period and archived to Amazon S3 for audit, analytics and recovery.

Handling Duplicate and Out-of-Order Prices

Distributed messaging systems normally provide at-least-once delivery. A price processor must therefore expect both duplicate and out-of-order events.

A vendor-provided sequence number is the best ordering mechanism. If one is unavailable, the system may use a vendor timestamp combined with a deterministic tie-breaker.

A timestamp alone is insufficient because:

  • vendor clocks may not be synchronized;

  • two events may have identical timestamps;

  • timestamps may use different precision;

  • network delays can change arrival order;

  • different vendors may have different notions of event time.

The price processor should use a conditional update so that an older event cannot overwrite a newer price.

Ordering should generally be evaluated within the combination of:

instrument + vendor + feed

Sequence numbers from two different vendors should not be directly compared unless their contract explicitly guarantees a common sequence.

The conditional update protects the latest-price record from out-of-order writes; it does not guarantee that a later reader will observe its own write. The Quote Service must therefore use the appropriate DynamoDB read consistency for the freshness-sensitive path. For a single-item read of the hot latest-price key, a strongly consistent read should be used when the service must immediately observe the latest accepted record. This is separate from the ordering guarantee: conditional writes prevent stale events from winning, while read consistency determines what a subsequent Quote Service read can observe.

Creating a Customer Quote

When a customer asks for a portfolio price, the Quote Service reads the required instrument prices from DynamoDB. These reads are observations taken at particular instants; they do not form an atomic snapshot across all instruments or across DynamoDB and Aurora. A price may change after it is read and before the quote is written. That is acceptable and intentional.

The safety of the quote does not come from promising that the price remained unchanged during the operation. It comes from storing the exact price and its priceVersion in the immutable Aurora quote. The priceVersion identifies the source sequence or equivalent version that was captured and used for calculation. The resulting quote therefore records which market observation became the business commitment, even if the latest-price record changes before or immediately after the Aurora transaction.

Before creating a quote, it validates:

  • every required instrument has a price;

  • each price is sufficiently fresh;

  • currencies and market sessions are correct;

  • no instrument has been suspended;

  • all pricing rules can be applied.

The freshness check should use a platform-controlled time basis. receivedAt, compared with the Quote Service’s trusted server time, should normally determine whether the record is within the two-second operational freshness window. vendorTimestamp should be retained for audit and event-time analysis, but should not normally be used as the sole basis for a strict freshness SLA because vendor clock skew can make a price appear newer or older than it is. If vendor time is used for any business rule, clock-skew bounds and validation must be explicitly defined.

The service should evaluate freshness for every instrument in the portfolio. Under the default all-or-nothing policy, a portfolio is unquotable if any required instrument is missing, suspended, invalid or outside the freshness threshold. There is no degraded or partial quote unless a separate product policy explicitly defines one. This policy should be confirmed with the product and risk owners because a portfolio containing many instruments has a higher probability that at least one instrument will be stale. If partial quoting is later desired, it must define how omitted or stale instruments affect totals, customer acceptance and order creation; it must not be introduced implicitly.

It then stores an immutable quote in Aurora PostgreSQL.

The quote should include:

  • quote ID;

  • customer and portfolio IDs;

  • instrument identifiers and quantities;

  • exact instrument prices;

  • price versions;

  • exchange rates;

  • fees;

  • calculated total;

  • pricing-rule version;

  • quote creation time;

  • expiry time;

  • source metadata.

Storing only the final portfolio total would not be sufficient. The platform must be able to explain exactly how that amount was calculated.

Price Freshness and Quote Expiry Are Different

Two related but separate time limits are involved.

Price freshness determines whether a market price is recent enough to create a new quote.

Quote validity determines how long the business promises to honour a quote after creating it.

For example:

Maximum acceptable market-price age: 2 seconds
Customer quote validity: 10 seconds

A three-second-old market price cannot be used to create a new quote, even if a successfully created quote remains valid for ten seconds.

This distinction is important because the first rule protects pricing quality, while the second represents a commercial commitment to the customer.

The freshness calculation should use receivedAt and a trusted server-side clock, with an explicitly documented allowance for infrastructure clock skew. vendorTimestamp remains useful for diagnosing vendor latency and validating feed behaviour, but vendor-supplied time must not silently become the authoritative clock for the freshness SLA.

Accepting a Quote

The ordering domain should own quote acceptance.

When the customer accepts a quote, the request includes the quote ID and an idempotency key. The Order Service then performs a single Aurora transaction:

  1. Verify that the quote belongs to the customer.

  2. Check that its status is ACTIVE.

  3. Check that it has not expired.

  4. Mark it as ACCEPTED.

  5. Create the order using the stored quote prices.

  6. Insert an event into the transactional outbox.

  7. Commit the transaction.

A conditional update can enforce the decision atomically:

UPDATE quote
SET status = 'ACCEPTED',
    accepted_at = CURRENT_TIMESTAMP
WHERE quote_id = :quoteId
  AND status = 'ACTIVE'
  AND expires_at >= CURRENT_TIMESTAMP;

If one row is updated, acceptance succeeded. If no rows are updated, the quote was expired, cancelled or previously accepted.

The server-side database clock must determine expiry. The browser countdown is only a user-interface aid and cannot be authoritative.

Why Not Use a Distributed Transaction?

Creating a quote involves reading from DynamoDB and writing to Aurora. There is no single ACID transaction across these two databases.

That is acceptable because the market price is an observation, while the stored quote is a business commitment.

The read and write are therefore intentionally non-atomic. Between reading a price from DynamoDB and writing the quote to Aurora, the latest price may update. The system does not attempt to prevent that update or claim that the read remained current. Instead, the quote stores the exact values and priceVersion values captured by the Quote Service. Those versions make the quote auditable and safe: they identify the market observations used for the commitment.

The DynamoDB conditional update protects the latest-price record from an older event overwriting a newer event. It does not provide read-your-write consistency to the Quote Service. That guarantee must come from the selected DynamoDB read mode, with strongly consistent reads used for the freshness-sensitive latest-price lookup where required.

The database transaction is required only when the quote becomes an order. Quote acceptance, order creation and outbox insertion all belong to the same consistency boundary and therefore occur in Aurora.

Reliable Business-Event Publication

The transactional outbox should use the standard pattern:

  1. The Aurora transaction changes the quote and order state.

  2. The same transaction inserts an outbox row containing the business event, event ID, aggregate ID, payload and publication status.

  3. A separate publisher polls the outbox or consumes database change data capture.

  4. The publisher sends the event to the target broker or service.

  5. Successful publication is recorded, and retries continue for failures.

The publisher must be idempotent because a crash can occur after publication but before the outbox row is marked as published. Consumers should deduplicate using the event ID.

This pattern makes both of the following recoverable:

  • if Aurora commits but the response is lost, a retry can return the already-created order using the idempotency key;

  • if event publication fails after the Aurora commit, the durable outbox row remains available for later retry.

The outbox is therefore part of the Aurora consistency boundary, not merely an asynchronous best-effort notification mechanism.

Vendor Failover

The second price vendor should not simply overwrite the primary vendor whenever it provides a later timestamp.

Failover should be controlled through explicit states:

PRIMARY_ACTIVE
FALLBACK_PENDING
FALLBACK_ACTIVE
PRIMARY_PENDING

Before activating the fallback feed, the platform should validate:

  • feed heartbeat;

  • instrument coverage;

  • price freshness;

  • bid/ask spread;

  • deviation from the last trusted primary price;

  • timestamp or sequence progression;

  • currency and trading-session information.

The same validation is required before switching back to the primary vendor. A recently recovered feed should remain stable for a defined period before it becomes authoritative again.

Important Failure Scenarios

A robust design must define its behaviour before these failures occur:

Scenario Expected behaviour
Price changes after quote creation Honour the stored quote until it expires
Price changes between DynamoDB read and Aurora write Store the captured values and priceVersion; do not require the price to remain unchanged
DynamoDB read observes an older record Use strongly consistent reads for the freshness-sensitive path where required
Customer accepts at the expiry boundary Aurora makes the authoritative decision
Customer submits twice Return the original result using the idempotency key
Two devices accept the same quote Only one conditional update succeeds
A stale price is found Refuse to create the quote and request repricing
One portfolio instrument has no price Reject the entire portfolio quote under the all-or-nothing policy
One portfolio instrument is stale Reject the entire portfolio quote under the all-or-nothing policy
Kafka delivers a duplicate Deduplicate using instrument, source and sequence
Kafka delivers an older event Reject it through a conditional write
A hot instrument overloads its partition Monitor lag and throughput; provision or isolate capacity according to the partition-skew policy
Primary vendor fails Validate the fallback before activating it
Aurora commits but the response is lost A retry returns the already-created order
Event publication fails after commit The transactional outbox publishes it later
Execution venue rejects the order Record the rejection without changing quote history

Advantages of the Two-Database Design

The separation provides several benefits:

  • market-data volume does not directly overload the order database;

  • each database is optimized for its workload;

  • accepted prices remain independent of later market movements;

  • the latest-price store can be reconstructed through event replay;

  • Aurora provides transactional order processing;

  • price ingestion and order processing can scale independently;

  • historical business decisions remain auditable.

The design also introduces costs:

  • there is no transaction across the two databases;

  • quote reads are not an atomic multi-instrument snapshot;

  • operational support becomes more complex;

  • separate backup and recovery plans are required;

  • cross-domain reporting needs an analytical pipeline;

  • teams must understand which system owns each fact;

  • all-or-nothing freshness can make large portfolios unquotable when one instrument is stale;

  • hot instruments may create uneven MSK partition load.

The solution is not to hide these limitations behind a distributed transaction. It is to define clear ownership and consistency boundaries.

Final Design Principle

The architecture can be summarized in one sentence:

Amazon MSK and DynamoDB manage continuously changing market state, while Aurora PostgreSQL records the immutable quotes and transactional orders that represent customer and business commitments.

The quote is not safe because its DynamoDB read and Aurora write are atomic, nor because the market price cannot change during quote creation. It is safe because the Quote Service validates the captured observations, records the exact values and priceVersion identifiers it used, and then treats the Aurora record as the immutable business commitment. DynamoDB ordering protects the latest-price state, while strongly consistent reads support the freshness decision where required.

This separation allows the price pipeline to remain fast and scalable without weakening the guarantees required for financial transactions.

The most important architectural decision is therefore not the choice between DynamoDB and PostgreSQL. It is deciding where market information ends and a business commitment begins.

Design

Part 1 of 1

Design