DEV Community

Cover image for Building a High-Throughput Transaction Processing System
Derek Mwale
Derek Mwale

Posted on

Building a High-Throughput Transaction Processing System

There is a moment in software engineering when CRUD stops being enough.

You can build an API.

You can build a database.

You can build authentication.

You can build a dashboard.

You can even put all of them together and call it a production system.

Then someone asks a different question:

“What happens when ten thousand things need to happen at the same time?”

That question changes everything.

A transaction processing system is not simply an application that writes rows to a database.

It is a machine for coordinating state.

Money moves.

Inventory changes.

Orders are created.

Accounts are debited.

Accounts are credited.

Tickets are reserved.

Balances change.

Events are emitted.

Retries happen.

Networks fail.

Processes crash.

Messages arrive twice.

Messages arrive out of order.

And somewhere in the middle of all this chaos, the system has to preserve one thing:

correctness.

Now make the system high-throughput.

Suddenly, the problem becomes much more interesting.

We are no longer asking:

How do I process a transaction?

We are asking:

How do I process millions of transactions while preserving correctness under concurrency, failures, retries, contention, and partial system failure?

That is a completely different engineering problem.

This article is about building such a system from first principles.

Not because every application needs one.

Most applications don't.

But because transaction processing teaches some of the deepest ideas in distributed systems:

  • concurrency control
  • atomicity
  • idempotency
  • partitioning
  • ordering
  • backpressure
  • batching
  • durability
  • replication
  • failure recovery
  • consistency
  • observability
  • workload isolation

Once you understand these ideas, you begin seeing them everywhere.

A payment API is a transaction processor.

An inventory engine is a transaction processor.

A banking ledger is a transaction processor.

An order system is a transaction processor.

A ticketing platform is a transaction processor.

Even some seemingly unrelated systems are secretly transaction processors.

The interface changes.

The mathematics doesn't.


1. First, Define What a Transaction Actually Is

Let's start with a simple model.

Suppose we have an account:

Account A
Balance = $1,000
Enter fullscreen mode Exit fullscreen mode

A transaction wants to withdraw:

$100
Enter fullscreen mode Exit fullscreen mode

The simplest representation is:

balance = balance - 100
Enter fullscreen mode Exit fullscreen mode

But a real transaction isn't just an arithmetic operation.

It has identity.

It has state.

It has inputs.

It has constraints.

It has side effects.

It may fail.

A useful abstraction is:

Transaction {
    id
    source
    destination
    amount
    currency
    timestamp
    metadata
}
Enter fullscreen mode Exit fullscreen mode

Then processing becomes something like:

Transaction
      |
      v
Validation
      |
      v
Authorization
      |
      v
State Transition
      |
      v
Persistence
      |
      v
Event Publication
Enter fullscreen mode Exit fullscreen mode

That looks straightforward.

But each arrow hides an engineering problem.

What if two transactions modify the same account simultaneously?

What if the process crashes after the database commit but before the event is published?

What if the client retries the request?

What if two workers process the same transaction?

What if the database is temporarily unavailable?

What if one customer produces 90% of the traffic?

What if the queue contains ten million transactions?

High throughput doesn't remove these problems.

It amplifies them.


2. The Core Architecture

A useful starting architecture looks like this:

                    Clients
                       |
                       v
                +--------------+
                | Load Balancer|
                +--------------+
                       |
                       v
                +--------------+
                | Transaction  |
                | API Layer    |
                +--------------+
                       |
              +--------+--------+
              |                 |
              v                 v
        Validation         Idempotency
              |                 |
              +--------+--------+
                       |
                       v
                +--------------+
                | Message      |
                | Queue        |
                +--------------+
                       |
              +--------+--------+
              |        |        |
              v        v        v
           Worker   Worker   Worker
              |        |        |
              +--------+--------+
                       |
                       v
                +--------------+
                | Transaction  |
                | Store        |
                +--------------+
                       |
                       v
                +--------------+
                | Event Stream |
                +--------------+
Enter fullscreen mode Exit fullscreen mode

The architecture separates ingestion from execution.

That distinction is extremely important.

The API should not necessarily perform all transaction work synchronously.

Instead, the API can accept a transaction, validate it, assign an identity, persist the necessary information, and place work into a durable queue.

Workers then process the transaction.

This creates a buffer between incoming demand and processing capacity.

That buffer is one of the most powerful ideas in high-throughput systems.


3. Throughput Is Not the Same as Speed

Engineers sometimes confuse latency and throughput.

They are related.

They are not identical.

Latency asks:

How long does one operation take?

Throughput asks:

How many operations can the system process per unit of time?

Suppose a transaction takes 20 milliseconds.

A single worker might process approximately:

1 / 0.020 = 50 transactions/sec
Enter fullscreen mode Exit fullscreen mode

But if we have 100 independent workers:

50 × 100 = 5,000 transactions/sec
Enter fullscreen mode Exit fullscreen mode

assuming the workload can actually be parallelized and the database doesn't become the bottleneck.

This introduces a fundamental principle:

High throughput usually comes from controlled parallelism, not from making one operation infinitely fast.

But uncontrolled parallelism is dangerous.

If 10,000 workers simultaneously update the same database row, you haven't created a 10,000× faster system.

You've created a contention machine.


4. The Database Is Often the Real Bottleneck

Consider:

UPDATE accounts
SET balance = balance - 100
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

This looks cheap.

Now imagine 50,000 transactions targeting account 42.

The transactions are logically independent from the perspective of the API.

They aren't necessarily independent from the perspective of the database.

They all want the same piece of state.

This creates a hot key.

A system can scale horizontally only when its workload can be distributed horizontally.

That gives us another important principle:

Parallelism is limited by shared mutable state.

This is one of the reasons transaction architecture often starts looking like distributed-systems architecture.


5. Atomicity Is Non-Negotiable

Imagine a transfer:

Alice:   $1,000
Bob:       $500
Enter fullscreen mode Exit fullscreen mode

Alice sends Bob $100.

Correct result:

Alice:     $900
Bob:       $600
Enter fullscreen mode Exit fullscreen mode

But imagine this sequence:

Debit Alice
      |
      X
   Process crashes
Enter fullscreen mode Exit fullscreen mode

Now:

Alice: $900
Bob:   $500
Enter fullscreen mode Exit fullscreen mode

The system created money destruction.

The opposite problem is even worse:

Credit Bob
      |
      X
Process crashes
Enter fullscreen mode Exit fullscreen mode

Now money appeared from nowhere.

This is why related state transitions must be atomic.

A relational database transaction might look like:

BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 'alice'
  AND balance >= 100;

UPDATE accounts
SET balance = balance + 100
WHERE id = 'bob';

INSERT INTO transactions (...);

COMMIT;
Enter fullscreen mode Exit fullscreen mode

Either the entire state transition commits or none of it does.

Atomicity gives us a boundary around the state transition.


6. But Database Transactions Don't Solve Everything

This is where architecture becomes interesting.

Suppose we commit:

Database
    |
    v
Transaction completed
Enter fullscreen mode Exit fullscreen mode

Then the process crashes before publishing:

Event Bus
Enter fullscreen mode Exit fullscreen mode

Now the database says:

SUCCESS
Enter fullscreen mode Exit fullscreen mode

but downstream systems never received the event.

Maybe the notification service doesn't know.

Maybe analytics doesn't know.

Maybe the inventory service doesn't know.

Maybe the external integration doesn't know.

This is a classic dual-write problem.

We have two systems:

Database
Message Broker
Enter fullscreen mode Exit fullscreen mode

and we want both to reflect one logical operation.

A naive implementation:

database.commit()
message_broker.publish(event)
Enter fullscreen mode Exit fullscreen mode

creates a failure window.

The process can die between those operations.

One common solution is the transactional outbox pattern.

Instead of directly publishing the event:

Database Transaction
       |
       +--> Business State
       |
       +--> Outbox Event
Enter fullscreen mode Exit fullscreen mode

Both are committed atomically.

Then a separate publisher reads the outbox:

Outbox
   |
   v
Publisher
   |
   v
Message Broker
Enter fullscreen mode Exit fullscreen mode

Now the event can be retried safely.

This is a recurring pattern in reliable transaction systems:

Put durable intent next to durable state.


7. Idempotency Is Your Shield Against Retries

Distributed systems retry.

They have to.

Networks fail.

Clients timeout.

Load balancers terminate connections.

Workers crash.

Queues redeliver messages.

But retries create a terrifying possibility.

Suppose the client sends:

POST /transfer

$100 Alice -> Bob
Enter fullscreen mode Exit fullscreen mode

The server processes it successfully.

But the response gets lost.

The client sees:

TIMEOUT
Enter fullscreen mode Exit fullscreen mode

So it retries.

Now the server receives the same logical transaction twice.

Without idempotency:

Alice -$100
Bob +$100

Alice -$100
Bob +$100
Enter fullscreen mode Exit fullscreen mode

The transaction happened twice.

The solution is a unique idempotency key:

Idempotency-Key:
8f2e4d...
Enter fullscreen mode Exit fullscreen mode

Store the result:

idempotency_key
transaction_id
status
response
Enter fullscreen mode Exit fullscreen mode

Then:

Request 1
   |
   v
Process transaction
   |
   v
Store result

Request 2
   |
   v
Same idempotency key
   |
   v
Return previous result
Enter fullscreen mode Exit fullscreen mode

This changes retry semantics.

Retries become safe.

And safe retries are one of the foundations of reliable distributed systems.


8. Idempotency Must Exist at Multiple Layers

A common mistake is thinking idempotency belongs only to the API.

It doesn't.

Consider:

API
 |
Queue
 |
Worker
 |
Database
 |
External Payment Provider
Enter fullscreen mode Exit fullscreen mode

Every layer can experience duplication.

The queue can deliver twice.

The worker can crash after executing the operation.

The external provider can timeout after accepting the transaction.

Therefore, idempotency often needs multiple identities.

For example:

client_request_id
transaction_id
execution_id
external_reference
Enter fullscreen mode Exit fullscreen mode

You might have:

Client Request
       |
       v
Transaction ID
       |
       v
Execution Attempt
       |
       v
External Operation
Enter fullscreen mode Exit fullscreen mode

The identities answer different questions.

The transaction ID answers:

What logical operation is this?

The execution ID answers:

Which attempt to execute it was this?

The external reference answers:

What should the external system consider unique?

Identity becomes an architectural primitive.


9. Queues Create Elasticity

Suppose your system normally processes:

5,000 transactions/sec
Enter fullscreen mode Exit fullscreen mode

but traffic suddenly becomes:

20,000 transactions/sec
Enter fullscreen mode Exit fullscreen mode

You have two choices.

Make the API synchronously process everything and watch latency explode.

Or introduce buffering.

Incoming Rate
      |
      v
+-------------+
|    Queue    |
+-------------+
      |
      v
Processing Rate
Enter fullscreen mode Exit fullscreen mode

The queue absorbs bursts.

If:

arrival rate > processing rate
Enter fullscreen mode Exit fullscreen mode

the queue grows.

If:

arrival rate < processing rate
Enter fullscreen mode Exit fullscreen mode

the queue shrinks.

This is essentially a pressure-management mechanism.

But queues don't magically solve capacity problems.

If the system receives 20,000 transactions/sec forever and can only process 5,000/sec, the queue will eventually become enormous.

That means queue depth itself becomes an important signal.


10. Backpressure

A high-throughput system must know when to say:

“Slow down.”

This is backpressure.

Without it, every layer pushes work downstream as quickly as possible.

Eventually:

API
  ↓
Queue
  ↓
Workers
  ↓
Database
  ↓
Database overload
  ↓
Timeouts
  ↓
Retries
  ↓
More traffic
  ↓
More overload
Enter fullscreen mode Exit fullscreen mode

This is a positive feedback loop.

The system begins amplifying its own failure.

A better architecture establishes limits:

Maximum queue size
Maximum concurrency
Maximum database connections
Maximum batch size
Maximum retry rate
Maximum transaction age
Enter fullscreen mode Exit fullscreen mode

When downstream capacity is exhausted, upstream components must respond accordingly.

Sometimes that means:

429 Too Many Requests
Enter fullscreen mode Exit fullscreen mode

Sometimes:

202 Accepted
Enter fullscreen mode Exit fullscreen mode

Sometimes traffic is throttled internally.

Sometimes low-priority work is delayed.

The important concept is:

A stable system must control the rate at which work enters constrained resources.


11. Partitioning

Now we reach one of the most important techniques for scaling transaction processing.

Partitioning.

Suppose we have:

100 million transactions
Enter fullscreen mode Exit fullscreen mode

We don't want every worker fighting over one processing stream.

Instead:

Partition 0
Partition 1
Partition 2
...
Partition N
Enter fullscreen mode Exit fullscreen mode

Transactions are assigned based on a key.

For example:

partition = hash(account_id) % N
Enter fullscreen mode Exit fullscreen mode

Then:

Account A → Partition 3
Account B → Partition 7
Account C → Partition 2
Enter fullscreen mode Exit fullscreen mode

This provides two things:

  1. parallelism
  2. localized ordering

And localized ordering is incredibly valuable.


12. Global Ordering Is Expensive

Imagine we require every transaction in the entire system to be globally ordered:

T1
T2
T3
T4
...
Enter fullscreen mode Exit fullscreen mode

This creates a serialization point.

Now imagine 100 workers.

They can't freely process transactions because the system must preserve one global sequence.

You have built a distributed system around a single bottleneck.

Instead, we often only need ordering where state conflicts.

For account-based transactions:

Account A:
T1 → T2 → T3

Account B:
T4 → T5 → T6
Enter fullscreen mode Exit fullscreen mode

There may be no reason to impose:

T1 < T4 < T2 < T5 < T3 < T6
Enter fullscreen mode Exit fullscreen mode

The system can process account A and account B independently.

This gives us a profound scalability principle:

Don't synchronize what doesn't need to be synchronized.


13. Concurrency Control

Now imagine two withdrawals:

Balance = $100
Enter fullscreen mode Exit fullscreen mode

Two workers simultaneously process:

Withdraw $80
Withdraw $70
Enter fullscreen mode Exit fullscreen mode

If both read:

balance = $100
Enter fullscreen mode Exit fullscreen mode

then both might conclude:

Enough funds.
Enter fullscreen mode Exit fullscreen mode

And both write their result.

Now we have:

Balance = $30
Enter fullscreen mode Exit fullscreen mode

even though the account spent:

$150
Enter fullscreen mode Exit fullscreen mode

This is a race condition.

There are multiple ways to solve it.

One approach is pessimistic locking:

SELECT balance
FROM accounts
WHERE id = ?
FOR UPDATE;
Enter fullscreen mode Exit fullscreen mode

The row is locked while the transaction executes.

Another is optimistic concurrency control.

Store a version:

balance = 100
version = 7
Enter fullscreen mode Exit fullscreen mode

Update:

UPDATE accounts
SET balance = 20,
    version = 8
WHERE id = ?
  AND version = 7;
Enter fullscreen mode Exit fullscreen mode

If zero rows are affected, another transaction modified the account first.

Retry.

Different workloads benefit from different approaches.

There is no universal concurrency-control strategy.


14. Ledger Versus Mutable Balance

One of the most important architectural decisions in financial systems is whether to treat the balance as the source of truth.

A mutable balance:

balance = 950
Enter fullscreen mode Exit fullscreen mode

is convenient.

But it doesn't tell us how we got there.

A ledger does:

+1000 opening
-100 transfer
-50 purchase
+200 deposit
-100 withdrawal
Enter fullscreen mode Exit fullscreen mode

Then:

Balance = SUM(entries)
Enter fullscreen mode Exit fullscreen mode

The ledger preserves history.

This introduces another principle:

State tells you where you are. Events tell you how you got there.

A mature transaction system often stores both.

For example:

accounts
---------
id
current_balance

ledger_entries
-------------
id
account_id
transaction_id
amount
direction
created_at
Enter fullscreen mode Exit fullscreen mode

The current balance can provide fast reads.

The ledger provides an auditable history.


15. Double-Entry Thinking

For money-like systems, double-entry accounting is extremely powerful.

Every transaction has two sides.

If Alice sends Bob $100:

Alice: -100
Bob:   +100
Enter fullscreen mode Exit fullscreen mode

The total is:

-100 + 100 = 0
Enter fullscreen mode Exit fullscreen mode

The transaction balances.

This gives us an invariant:

SUM(all ledger movements for a transaction) = 0
Enter fullscreen mode Exit fullscreen mode

In a system with millions of transactions, invariants are invaluable.

Instead of trying to prove that every operation is correct through inspection, we continuously verify mathematical properties.

For example:

debits == credits
Enter fullscreen mode Exit fullscreen mode

or:

available_balance >= 0
Enter fullscreen mode Exit fullscreen mode

or:

transaction state cannot move backward
Enter fullscreen mode Exit fullscreen mode

These become automated integrity checks.

Mathematics becomes part of the monitoring system.


16. State Machines

A transaction shouldn't be represented simply as:

status = "done"
Enter fullscreen mode Exit fullscreen mode

A better model is a state machine:

PENDING
   |
   v
PROCESSING
   |
   +------> FAILED
   |
   v
COMPLETED
Enter fullscreen mode Exit fullscreen mode

Maybe:

PENDING
   |
   v
AUTHORIZED
   |
   v
CAPTURED
   |
   v
SETTLED
Enter fullscreen mode Exit fullscreen mode

The important part is controlling valid transitions.

For example:

COMPLETED → PENDING
Enter fullscreen mode Exit fullscreen mode

should probably be invalid.

The state machine can enforce:

current_state
+
event
=
next_state
Enter fullscreen mode Exit fullscreen mode

Conceptually:

next_state = transition(
    current_state,
    event
)
Enter fullscreen mode Exit fullscreen mode

This prevents random pieces of application code from mutating transaction status however they want.


17. Batching

High-throughput systems often benefit enormously from batching.

Instead of:

INSERT transaction
INSERT transaction
INSERT transaction
INSERT transaction
Enter fullscreen mode Exit fullscreen mode

we can perform:

INSERT 1,000 transactions
Enter fullscreen mode Exit fullscreen mode

This reduces:

  • network round trips
  • transaction overhead
  • parsing overhead
  • connection overhead
  • filesystem operations

A worker might accumulate:

Batch = 500 transactions
Enter fullscreen mode Exit fullscreen mode

or:

Batch = 10 ms window
Enter fullscreen mode Exit fullscreen mode

and then process them together.

But batching creates a tradeoff.

Larger batches improve throughput.

Smaller batches reduce latency.

So you might configure:

batch_size = 500
max_wait = 10ms
Enter fullscreen mode Exit fullscreen mode

Process immediately when:

batch.size >= 500
Enter fullscreen mode Exit fullscreen mode

or when:

oldest_item_age >= 10ms
Enter fullscreen mode Exit fullscreen mode

This is a classic throughput-latency tradeoff.


18. Connection Pools

You can't have unlimited database connections.

Suppose:

500 workers
Enter fullscreen mode Exit fullscreen mode

and every worker opens:

20 DB connections
Enter fullscreen mode Exit fullscreen mode

You now have:

10,000 database connections
Enter fullscreen mode Exit fullscreen mode

The database might collapse before your application does.

A connection pool limits concurrency:

Workers
   |
   v
Connection Pool
   |
   +---- Connection 1
   +---- Connection 2
   +---- Connection 3
   +---- ...
Enter fullscreen mode Exit fullscreen mode

The pool becomes another bounded resource.

This is good.

Boundaries create stability.

You might discover that 200 application workers only need 100 database connections.

More isn't automatically better.


19. CPU, Memory, Network, or Database?

When throughput is low, don't immediately add servers.

First determine the bottleneck.

Possible constraints include:

CPU
Memory
Disk I/O
Network
Database locks
Database connections
Queue throughput
Serialization
Encryption
External APIs
Garbage collection
Enter fullscreen mode Exit fullscreen mode

A useful mental model is:

Throughput ≈ min(
    API capacity,
    queue capacity,
    worker capacity,
    database capacity,
    external dependency capacity
)
Enter fullscreen mode Exit fullscreen mode

The slowest constrained component controls the system.

Adding ten API servers does nothing if the database can only process the workload generated by two.


20. Avoid Unnecessary Work

High throughput is often less about doing things faster and more about doing fewer things.

Ask:

Do I need this database query?
Do I need this serialization?
Do I need this network request?
Do I need this lock?
Do I need this event?
Do I need this index?
Do I need this synchronous dependency?
Enter fullscreen mode Exit fullscreen mode

Suppose processing one transaction performs:

5 SQL queries
3 Redis calls
2 HTTP calls
4 serialization operations
Enter fullscreen mode Exit fullscreen mode

You might spend enormous engineering effort optimizing each operation.

But perhaps the real optimization is:

5 SQL → 2 SQL
3 Redis → 1 Redis
2 HTTP → 0 HTTP
Enter fullscreen mode Exit fullscreen mode

Removing work often beats optimizing work.


21. Fast Path and Slow Path

Not every transaction requires identical processing.

You can separate:

Fast Path
Enter fullscreen mode Exit fullscreen mode

from:

Slow Path
Enter fullscreen mode Exit fullscreen mode

For example:

Transaction
    |
    +--> Simple local transfer
    |       |
    |       v
    |   Fast Path
    |
    +--> Fraud investigation
            |
            v
        Slow Path
Enter fullscreen mode Exit fullscreen mode

This prevents expensive operations from blocking simple operations.

A high-throughput system should avoid letting rare expensive transactions determine the latency of ordinary ones.


22. Priority Queues

Not all work has equal urgency.

You might have:

HIGH
NORMAL
LOW
Enter fullscreen mode Exit fullscreen mode

For example:

Settlement → HIGH
Normal payment → NORMAL
Analytics export → LOW
Enter fullscreen mode Exit fullscreen mode

But priority systems need care.

If HIGH traffic is unlimited, LOW traffic can starve.

One solution is weighted scheduling:

60% high
30% normal
10% low
Enter fullscreen mode Exit fullscreen mode

Another is aging:

priority increases as waiting time increases
Enter fullscreen mode Exit fullscreen mode

Again, the goal is controlled behavior under load.


23. Retry Carefully

Retries are necessary.

Uncontrolled retries are dangerous.

Imagine:

Database fails
Enter fullscreen mode Exit fullscreen mode

Every worker immediately retries.

Then:

Database recovers partially
       |
       v
10,000 retries
       |
       v
Database overloads again
Enter fullscreen mode Exit fullscreen mode

This is a retry storm.

Use:

exponential backoff
Enter fullscreen mode Exit fullscreen mode

For example:

100ms
200ms
400ms
800ms
1.6s
3.2s
Enter fullscreen mode Exit fullscreen mode

with jitter:

delay = exponential_backoff + random_jitter
Enter fullscreen mode Exit fullscreen mode

This spreads retries across time.

Also distinguish:

retryable failure
Enter fullscreen mode Exit fullscreen mode

from:

permanent failure
Enter fullscreen mode Exit fullscreen mode

Don't retry:

invalid account
invalid amount
authorization denied
Enter fullscreen mode Exit fullscreen mode

forever.

Retry:

temporary network error
database timeout
service unavailable
Enter fullscreen mode Exit fullscreen mode

when appropriate.


24. Dead-Letter Queues

Eventually some transactions cannot be processed automatically.

Instead of endlessly retrying:

Transaction
   |
   v
Retry
   |
   v
Retry
   |
   v
Retry
Enter fullscreen mode Exit fullscreen mode

move it into:

Dead Letter Queue
Enter fullscreen mode Exit fullscreen mode

Then operators or recovery processes can inspect it.

A dead-letter record might contain:

transaction_id
failure_reason
attempt_count
first_failed_at
last_failed_at
payload
Enter fullscreen mode Exit fullscreen mode

This creates a controlled failure boundary.

The transaction isn't silently lost.

It is isolated for recovery.


25. Exactly Once Is Usually a Dangerous Phrase

Distributed systems engineers should be careful with:

“exactly once.”

At the business level, you may want:

This payment should have exactly one financial effect.

But technically, the underlying system might execute a message multiple times.

A better architecture is often:

At-least-once delivery
+
Idempotent processing
=
Exactly-once business effect
Enter fullscreen mode Exit fullscreen mode

This distinction matters.

You don't necessarily need exactly-once execution.

You need exactly-once semantics where it matters.

That is a much more achievable engineering goal.


26. Observability

A transaction system without observability is a black box.

You need metrics such as:

transactions_received_total
transactions_completed_total
transactions_failed_total
transactions_retried_total
transaction_latency
queue_depth
worker_utilization
database_latency
lock_wait_time
Enter fullscreen mode Exit fullscreen mode

And importantly:

p50
p95
p99
p99.9
Enter fullscreen mode Exit fullscreen mode

Average latency can lie.

Imagine:

99,000 transactions = 10ms
1,000 transactions = 10 seconds
Enter fullscreen mode Exit fullscreen mode

The average may appear reasonable.

But 1% of users are having a terrible experience.

Tail latency matters.


27. Trace the Transaction

Give every transaction a correlation identity:

transaction_id
Enter fullscreen mode Exit fullscreen mode

Then trace:

API
 ↓
Queue
 ↓
Worker
 ↓
Database
 ↓
Outbox
 ↓
Event Bus
 ↓
Downstream Service
Enter fullscreen mode Exit fullscreen mode

If transaction:

TX-92831
Enter fullscreen mode Exit fullscreen mode

fails, you should be able to answer:

Where did it fail?
How long did it wait?
How many times was it retried?
Which worker processed it?
Which database operation was slow?
Was the event published?
Enter fullscreen mode Exit fullscreen mode

Observability should answer questions before humans have to manually reconstruct the story.


28. Build Around Invariants

This is one of the most powerful approaches to transaction-system engineering.

Instead of only asking:

Does this endpoint return 200?

Ask:

What must always be true?

Examples:

Every transaction has a unique ID.

Every completed transaction has a durable record.

Every ledger transaction balances.

A completed transaction never returns to pending.

A transaction cannot be applied twice.

A debit cannot exceed available funds.

Every committed transaction has an auditable history.

Every accepted request can eventually be reconciled.
Enter fullscreen mode Exit fullscreen mode

These are system invariants.

Then build tests and monitoring around them.

A transaction processor becomes much easier to reason about when correctness is expressed as properties.


29. Testing Concurrency

Traditional unit tests are not enough.

A transaction system can pass:

100 sequential tests
Enter fullscreen mode Exit fullscreen mode

and fail immediately under:

100 concurrent requests
Enter fullscreen mode Exit fullscreen mode

You need concurrency tests.

For example:

Initial balance = $1,000

100 workers
each attempt to withdraw $20
Enter fullscreen mode Exit fullscreen mode

Expected:

successful withdrawals <= 50
Enter fullscreen mode Exit fullscreen mode

and:

final balance >= 0
Enter fullscreen mode Exit fullscreen mode

Then increase concurrency.

Try:

1
10
100
1,000
10,000
Enter fullscreen mode Exit fullscreen mode

Observe:

throughput
latency
lock contention
failure rate
Enter fullscreen mode Exit fullscreen mode

Load testing should not only measure speed.

It should test correctness under pressure.


30. Failure Testing

Now kill things.

Kill a worker during processing.

Kill it after the database commit.

Kill it before the commit.

Disconnect the database.

Delay the queue.

Duplicate messages.

Reorder messages.

Make downstream APIs timeout.

Fill the disk.

Exhaust connection pools.

Restart nodes.

These aren't pathological edge cases.

They are normal distributed-system events.

The question isn't:

Can the system avoid failure?

It can't.

The question is:

What state does the system reach after failure?

That is the real engineering question.


31. A Simplified Worker

A conceptual worker might look like:

def process(message):
    tx = load_transaction(message.transaction_id)

    if tx.status == "COMPLETED":
        return

    if already_processed(tx.id):
        return

    validate(tx)

    begin_transaction()

    try:
        apply_state_transition(tx)

        record_ledger_entries(tx)

        mark_completed(tx)

        create_outbox_event(tx)

        commit()

    except RetryableError:
        rollback()
        raise

    except Exception:
        rollback()
        mark_failed(tx)
Enter fullscreen mode Exit fullscreen mode

The actual implementation is significantly more complicated.

But the structure reveals the architecture:

load
check
validate
begin
mutate
record
complete
publish intent
commit
Enter fullscreen mode Exit fullscreen mode

The important part is that correctness is explicit.


32. Scaling Workers

Suppose one worker handles:

500 transactions/sec
Enter fullscreen mode Exit fullscreen mode

You deploy:

20 workers
Enter fullscreen mode Exit fullscreen mode

In an ideal world:

10,000 transactions/sec
Enter fullscreen mode Exit fullscreen mode

But real systems aren't ideal.

You may get:

8,200 transactions/sec
Enter fullscreen mode Exit fullscreen mode

because of:

  • database contention
  • uneven partition distribution
  • network overhead
  • garbage collection
  • queue coordination
  • locks
  • hot keys

This is why benchmarking matters.

Never assume:

2× workers = 2× throughput
Enter fullscreen mode Exit fullscreen mode

Measure it.


33. Hot Partitions

Partitioning introduces another problem.

Suppose:

Partition 1 → 90% of traffic
Partition 2 → 2%
Partition 3 → 2%
...
Enter fullscreen mode Exit fullscreen mode

Your system technically has 20 partitions.

But operationally, you only have one busy partition.

This is partition skew.

Possible solutions include:

  • better partition keys
  • virtual shards
  • splitting hot entities
  • workload-specific routing
  • dedicated processing lanes

But beware of splitting state that actually requires ordering.

You can't eliminate consistency requirements simply because they are inconvenient.


34. Horizontal Scaling Has a Mathematical Boundary

Suppose:

API = 100k req/sec
Queue = 200k msg/sec
Workers = 150k tx/sec
Database = 40k tx/sec
Enter fullscreen mode Exit fullscreen mode

System throughput is approximately constrained by:

40k tx/sec
Enter fullscreen mode Exit fullscreen mode

The database is the bottleneck.

Adding:

100 API servers
Enter fullscreen mode Exit fullscreen mode

doesn't fix it.

Adding:

1,000 workers
Enter fullscreen mode Exit fullscreen mode

doesn't fix it.

You need to change the constrained resource.

Perhaps:

partition database
Enter fullscreen mode Exit fullscreen mode

or:

reduce database work
Enter fullscreen mode Exit fullscreen mode

or:

batch writes
Enter fullscreen mode Exit fullscreen mode

or:

move non-critical work out of the transaction path
Enter fullscreen mode Exit fullscreen mode

Performance engineering begins by identifying constraints.


35. Synchronous Versus Asynchronous Transactions

Some transactions must return immediately.

Others don't.

You can expose:

POST /transactions
Enter fullscreen mode Exit fullscreen mode

and return:

{
  "transaction_id": "TX-92831",
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

The client can later query:

GET /transactions/TX-92831
Enter fullscreen mode Exit fullscreen mode

or subscribe to an event.

This transforms the API from:

request → complete operation → response
Enter fullscreen mode Exit fullscreen mode

into:

request → durable acceptance → asynchronous processing
Enter fullscreen mode Exit fullscreen mode

This can dramatically increase system resilience.

But it changes the user experience.

Architecture is always a negotiation between system properties.


36. Durability

High throughput is meaningless if accepted transactions disappear.

Durability means that once the system claims something is committed, it should survive process failure.

Depending on the architecture, durability might involve:

Write-ahead logs
Replication
Durable queues
Database persistence
Snapshots
Backups
Enter fullscreen mode Exit fullscreen mode

A high-throughput architecture often has multiple durability boundaries.

For example:

API
 |
Durable Queue
 |
Worker
 |
Database WAL
 |
Replica
Enter fullscreen mode Exit fullscreen mode

Each layer protects against different failure modes.


37. Replication

Read-heavy systems can often scale reads using replicas.

For example:

Primary
   |
   +--> Replica 1
   +--> Replica 2
   +--> Replica 3
Enter fullscreen mode Exit fullscreen mode

But transaction writes generally need careful coordination with the primary state.

And replicas introduce lag.

If:

write → primary
read → replica
Enter fullscreen mode Exit fullscreen mode

immediately afterward, the read may not see the write.

This is eventual consistency.

Sometimes that's acceptable.

Sometimes it isn't.

For transaction-critical operations, you need to explicitly identify where strong consistency is required.


38. Don't Put Everything in the Critical Path

Imagine a transaction requires:

fraud analysis
email notification
analytics
recommendation update
search indexing
webhook delivery
Enter fullscreen mode Exit fullscreen mode

Putting all of that into the synchronous transaction path creates unnecessary latency and failure coupling.

Instead:

Critical Path
-----------------
validate
authorize
commit state
record transaction

Async Path
-----------------
email
analytics
search
recommendations
webhooks
Enter fullscreen mode Exit fullscreen mode

The transaction processor should protect the smallest possible critical section.

Everything else can react asynchronously.


39. The Transaction Log Becomes the Spine

At scale, the transaction stream becomes more than a queue.

It becomes a system of record for what happened.

Conceptually:

Transaction Stream
       |
       +---- Ledger
       |
       +---- Analytics
       |
       +---- Notifications
       |
       +---- Fraud Detection
       |
       +---- Reporting
       |
       +---- Reconciliation
Enter fullscreen mode Exit fullscreen mode

One transaction can generate many downstream effects.

This is where event-driven architecture becomes powerful.

The transaction processor establishes the authoritative state transition.

Other systems consume the resulting facts.


40. Reconciliation

A serious transaction system should assume that eventually something will disagree.

Maybe:

internal ledger = $10,000
external provider = $9,950
Enter fullscreen mode Exit fullscreen mode

Now what?

You need reconciliation.

Compare:

Internal Transactions
        |
        v
External Transactions
        |
        v
Matching Engine
        |
        +---- MATCH
        |
        +---- MISMATCH
Enter fullscreen mode Exit fullscreen mode

Mismatches become explicit cases.

This is particularly important when integrating with external payment providers, banks, marketplaces, or financial systems.

Reconciliation is essentially another transaction-processing system operating on historical transactions.


41. Security

High throughput cannot come at the expense of security.

Every transaction should be associated with:

authenticated principal
authorization context
transaction identity
audit information
Enter fullscreen mode Exit fullscreen mode

Sensitive operations should be auditable.

Don't log secrets.

Don't log credentials.

Don't blindly log complete payment payloads.

Use:

structured logs
redaction
encryption
access controls
key rotation
Enter fullscreen mode Exit fullscreen mode

And protect administrative operations with stronger controls.

A transaction processor is effectively a machine that changes valuable state.

That makes it a high-value attack surface.


42. Rate Limiting

Suppose one client sends:

500,000 transactions/sec
Enter fullscreen mode Exit fullscreen mode

while everyone else sends:

100 transactions/sec
Enter fullscreen mode Exit fullscreen mode

Without limits, one client can dominate the system.

Rate limiting can exist at several layers:

User
API key
Tenant
IP
Account
Transaction type
Enter fullscreen mode Exit fullscreen mode

You might implement token bucket logic:

capacity = 10,000
refill_rate = 1,000/sec
Enter fullscreen mode Exit fullscreen mode

The objective isn't simply to reject traffic.

It is to protect system capacity.


43. Multi-Tenancy

For SaaS transaction systems, tenants create another dimension.

Imagine:

Tenant A → 80%
Tenant B → 10%
Tenant C → 5%
Tenant D → 5%
Enter fullscreen mode Exit fullscreen mode

Tenant A becomes a noisy neighbor.

One tenant should not be able to consume all workers, queue capacity, or database connections.

Possible controls include:

per-tenant quotas
per-tenant queues
weighted scheduling
tenant-specific rate limits
resource isolation
Enter fullscreen mode Exit fullscreen mode

Multi-tenancy is fundamentally a resource-allocation problem.


44. Designing the Transaction API

A minimal API might look like:

POST /v1/transactions
Enter fullscreen mode Exit fullscreen mode

Request:

{
  "source": "account_123",
  "destination": "account_456",
  "amount": 10000,
  "currency": "ZMW"
}
Enter fullscreen mode Exit fullscreen mode

Headers:

Authorization: Bearer ...
Idempotency-Key: 8f2e4d...
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "transaction_id": "tx_92831",
  "status": "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

Then:

GET /v1/transactions/tx_92831
Enter fullscreen mode Exit fullscreen mode

returns:

{
  "transaction_id": "tx_92831",
  "status": "COMPLETED"
}
Enter fullscreen mode Exit fullscreen mode

Notice what the API does not expose.

It doesn't expose internal worker details.

It doesn't expose queue partitions.

It doesn't expose database implementation.

The API describes the domain.

The infrastructure remains an implementation detail.


45. A Practical Architecture

A production architecture might eventually look like:

                         CLIENTS
                            |
                            v
                    +---------------+
                    | API Gateway   |
                    +---------------+
                            |
                            v
                    +---------------+
                    | Transaction   |
                    | API           |
                    +---------------+
                       |         |
                       |         +------> Idempotency Store
                       |
                       v
                +------------------+
                | Durable Queue    |
                +------------------+
                  |   |   |   |   |
                  v   v   v   v   v
                W1  W2  W3  W4  W5
                  \   |   |   |  /
                   \  |   |   | /
                    v v   v   v
                +------------------+
                | Transaction       |
                | Database          |
                +------------------+
                       |
              +--------+--------+
              |                 |
              v                 v
           Ledger            Outbox
                               |
                               v
                         Event Stream
                               |
              +----------------+----------------+
              |                |                |
              v                v                v
          Analytics        Webhooks         Notifications
Enter fullscreen mode Exit fullscreen mode

This isn't the only architecture.

It is simply a useful mental model.

The important boundaries are:

Ingress
Buffer
Execution
State
Durability
Events
Consumers
Enter fullscreen mode Exit fullscreen mode

46. The Deepest Problem: Contention

Eventually, most high-throughput transaction systems encounter the same enemy.

Contention.

Two operations want the same thing.

Two workers want the same row.

Two transactions want the same account.

Two services want the same lock.

Two requests want the same resource.

The system cannot parallelize a fundamentally serial operation.

This means performance engineering often becomes an exercise in reducing contention.

Instead of asking:

How can I make this operation faster?

Ask:

How can I make fewer operations compete with each other?

That question is much more powerful.


47. The Architecture Is Really About Time

At first glance, transaction processing appears to be about data.

But underneath, it is about time.

Consider:

Transaction A happens before B.
Enter fullscreen mode Exit fullscreen mode

or:

A and B happen concurrently.
Enter fullscreen mode Exit fullscreen mode

or:

A arrives after B but was created before B.
Enter fullscreen mode Exit fullscreen mode

or:

A is retried after the original attempt succeeded.
Enter fullscreen mode Exit fullscreen mode

or:

A's event arrives before B's event.
Enter fullscreen mode Exit fullscreen mode

Distributed systems force us to reason about temporal relationships.

This is why ordering, timestamps, sequence numbers, versions, and state transitions become so important.


48. From Transactions to a General-Purpose Engine

Once you understand the architecture, you can generalize it.

A transaction processor can become:

Command
   |
   v
Validation
   |
   v
Scheduling
   |
   v
Execution
   |
   v
State Transition
   |
   v
Event
Enter fullscreen mode Exit fullscreen mode

Replace "payment" with:

inventory adjustment
order creation
subscription change
ticket reservation
resource allocation
Enter fullscreen mode Exit fullscreen mode

The architecture remains surprisingly similar.

The domain changes.

The machinery doesn't change as much as people think.


49. What I Would Build First

If I were building a transaction engine from scratch, I wouldn't begin with Kubernetes.

I wouldn't begin with ten microservices.

I wouldn't begin with distributed databases.

I'd begin with a correct single-node implementation.

Something like:

API
 |
Transaction Service
 |
Database
 |
Outbox
Enter fullscreen mode Exit fullscreen mode

First establish:

atomicity
idempotency
state machine
ledger correctness
auditability
Enter fullscreen mode Exit fullscreen mode

Then benchmark it.

Then identify bottlenecks.

Then introduce:

queue
workers
partitioning
batching
replication
Enter fullscreen mode Exit fullscreen mode

Only when measurement proves the need.

This is important because distributed systems multiply complexity.

You should not distribute a system before understanding the system you're distributing.


50. The Final Architecture Principle

A high-throughput transaction processor is not fundamentally a fast database.

It is not fundamentally a queue.

It is not fundamentally a collection of workers.

It is a carefully constructed machine for controlling state transitions under concurrency.

Its architecture is built around a few fundamental ideas:

Identity
Atomicity
Idempotency
Ordering
Partitioning
Durability
Backpressure
Concurrency Control
Observability
Recovery
Enter fullscreen mode Exit fullscreen mode

And beneath all of them is one question:

What must always remain true?

That question is more important than:

How many requests per second can we handle?

Because a system that processes one million transactions per second incorrectly is not a high-throughput system.

It is a high-speed disaster.

The goal is not merely:

more transactions
Enter fullscreen mode Exit fullscreen mode

The goal is:

more correct transactions
Enter fullscreen mode Exit fullscreen mode

under:

more concurrency
more traffic
more failures
more retries
more machines
more users
Enter fullscreen mode Exit fullscreen mode

That is the real engineering challenge.


Conclusion

Building a high-throughput transaction processing system forces you to confront the uncomfortable parts of software engineering.

Concurrency.

Failure.

Ordering.

Durability.

Consistency.

Contention.

Retries.

Backpressure.

And the strange reality that the fastest architecture is often the one that avoids doing unnecessary work.

The journey usually begins with something simple:

Request
   ↓
Transaction
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Then reality arrives.

You need retries.

So you add idempotency.

You need bursts.

So you add queues.

You need throughput.

So you add workers.

You need ordering.

So you partition.

You need correctness.

So you introduce state machines and invariants.

You need reliable events.

So you add an outbox.

You need history.

So you introduce a ledger.

You need resilience.

So you add recovery and reconciliation.

Eventually, the architecture becomes something much more interesting than an API.

It becomes a machine that continuously transforms one state of the world into another.

And that is what transaction processing really is.

Not:

INSERT INTO transactions
Enter fullscreen mode Exit fullscreen mode

but:

WORLD₁
  |
  | transaction
  v
WORLD₂
Enter fullscreen mode Exit fullscreen mode

The engineering challenge is making that transformation:

correct
durable
observable
recoverable
idempotent
scalable
Enter fullscreen mode Exit fullscreen mode

even when thousands of machines are executing thousands of operations simultaneously.

That is where high-throughput systems become fascinating.

Because at sufficient scale, you stop programming individual requests.

You start programming time, state, concurrency, and failure.

And once you learn to think that way, you can build much more than transaction systems.

You can build the machinery underneath the modern internet.

Top comments (0)