---
title: "Compared to Database"
description: "Most backends keep their state in a database and build each process around it."
canonical_url: "https://docs.superdurable.io/intro/compared-to-database/"
source: "docs/content/intro/compared-to-database.mdx"
locale: "en"
---

# Compared to Database

Most backends keep their state in a database and build each process around it.
Below, one order process is built that way, one design at a time, and then as a Dex Flow.

The full order process charges the buyer and ships the item.
It waits for the seller's approval before shipping and reminds the seller while it waits.
It refunds the buyer when shipping still fails after an hour.
These are the parts each design needs for it:

Database and queues: 14 parts you build and run. Tables: an orders table with a status column and an approvals table. Queues: charge-requests, ship-requests, refund-requests, retry topics or visibility timeouts, and a dead-letter queue (DLQ). Services and jobs: charge, ship, and refund consumers, a reminder cron job, an approval endpoint, a status polling endpoint, and DLQ inspect and repair tools. They run on a database and a queue broker. Dex: 5 parts you write and run. One Flow with a Charge Step, a Ship Step, a Refund Step, and an approve RPC, and the Workers that host the Flow. They run on Dex Server, which provides retries per Step, durable Timers, waits on Channels, the Client wait API, and Step history with rerun.

## What a database does not do

Databases and storage services such as PostgreSQL, MySQL, and S3 store data and return it when you ask.
They are passive: they act only when your code calls them.

A reliable backend needs more than storage.
It runs on several machines, so that one failure does not stop it and it can handle more traffic.
One API often calls several other APIs and services, so one database transaction cannot cover it.

A typical backend API also:

- Calls several APIs, in sequence or in parallel.
- Schedules future work, such as reminders and business timeouts.
- Waits for other systems, such as a human approval or an external event.
- Handles failures with retries, rollback, and a dead-letter queue (DLQ) for manual repair. Each step may need a different policy.
- Shows the inputs, errors, and in-progress work of each step.
- Stays available, fast, and affordable as traffic grows.

A database provides none of this.
Teams write that code again for every service.

## The example: process an order in two steps

The order process has two steps, and each step is one API call:

- **Charge the buyer.**
- **Ship the item.**

A prototype calls the two APIs in a row:

**Python**

```python title="process_order.py"
def process_order(order):
    call_charge_api(order)
    call_ship_api(order)
```

**Go**

```go title="process_order.go"
func ProcessOrder(order Order) error {
	if err := callChargeAPI(order); err != nil {
		return err
	}
	return callShipAPI(order)
}
```

**Java**

```java title="OrderProcessor.java"
void processOrder(Order order) {
    callChargeApi(order);
    callShipApi(order);
}
```

**TypeScript**

```typescript title="process-order.ts"
async function processOrder(order: Order): Promise<void> {
  await callChargeApi(order);
  await callShipApi(order);
}
```

**Rust**

```rust title="process_order.rs"
fn process_order(order: &Order) -> Result<(), ApiError> {
    call_charge_api(order)?;
    call_ship_api(order)
}
```

This works until something fails.
If the process crashes after the charge succeeds, or the ship call times out, the buyer is charged but the item never ships.

A direct request runs several operations in sequence without a durable progress record. A crash can leave earlier operations complete while later operations never run.

### Design 1: a database row and polling workers

The next idea is to insert an order row with **status = pending**, return to the client, and let background workers pick up the row.

The frontend API inserts an order row with status pending and returns a request id. Worker A claims rows with SELECT … FOR UPDATE while Worker B scans the same table, so two Workers compete for the rows of one orders table.

The order is now saved, but new problems appear:

- How do several workers claim rows without taking the same one?
- What happens when a worker crashes after it claims a row but before it finishes?

### Design 2: a message queue between steps

Experienced teams usually move to message queues, one for each step: **charge-requests** and **ship-requests**.
An order starts as a message on **charge-requests**.
The charge consumer handles it and publishes a message to **ship-requests**.

A message queue passes work between consumers that perform successive operations. Each consumer must coordinate its operation with message acknowledgement to handle failures.

Queues solve the problems of Design 1, but they bring harder ones:

- Most queues cannot commit "charge done" and "enqueue ship" together. If the ship message is published but the acknowledgement of the charge message times out, the queue delivers the charge message again. The charge step then runs again while the ship step runs for the same order.
  - Teams usually add a database row as the source of truth to drop duplicates.
- Retries with backoff and a DLQ are often missing.
  - For example, Kafka needs separate retry topics, and SQS needs a visibility timeout as a workaround.
  - You also have to build tools to inspect and recover messages in the DLQ.
- The frontend may need to return as soon as the charge succeeds ("payment received, order processing"). It then has to poll the database, which is clumsy and wasteful.

The business also changes over time. The order process often needs to:

- Refund the buyer when the ship API still fails after retries, for example after an hour of failures.
- Wait for seller approval before shipping, or let the seller enter the tracking number by hand.
- Remind the seller to approve or to enter the tracking number.

Each of these adds queues, consumers, tables, or cron jobs to the queue design.

## The same process as a Dex Flow

In Dex you write only the business logic.
Dex provides the retries, timers, waits, crash recovery, and the record of each Step.

The frontend API starts the Flow and waits for the Charge Step with the Client API. Dex Server records each Step result and the move to the next Step together. It provides retries per Step, durable Timers, waits on Channels, and Step history with rerun. It sends each Step task to one of your Workers, and the Steps call the charge, ship, and refund APIs.

The charge, approve, and ship process becomes:

1. **Charge Step**: charge the buyer, with its own retry policy. When the charge succeeds, Dex moves the order to the Ship Step. Dex records the result and the move together, so a crash cannot leave the order between the two Steps.
2. **Ship Step**:
   1. Wait for the seller's approval **or** a durable timer.
   2. When the timer fires, send a reminder and keep waiting. When the approval arrives, ship the item.
   3. Retry the ship call. When the retries run out, move to the Refund Step.
3. **Refund Step**: refund the buyer.
4. The frontend calls a built-in Client API that waits until the Charge Step completes, then shows "payment received".

**Flow: OrderProcessingFlow**

Start Step: `Charge`

**Steps**

- `Charge`: Charge the customer for the order.
- `Refund`: Refund the order when shipping keeps failing.
  - `gracefulComplete`
- `Ship`: Wait for seller approval, remind after 24 hours, then ship the order.
  - WaitFor `anyOf`: channel `seller-ok.for 1`, timer `24 hours timer`
  - `gracefulComplete` when `not (context.has_timer_fired())`

**Edges**

- `goTo` (decision; Charge) → `Ship` (step): go_to
- `goTo` (decision; Ship; when `context.has_timer_fired()`) → `Ship` (step): go_to
- `order-status` (attribute) → `describe` (rpc): get
- `seller-ok` (channel) → `anyOf` (wait; Ship): seller-ok.for 1
- `approve` (rpc) → `seller-ok` (channel): publish
- `Charge` (step) → `order-status` (attribute): set
- `Refund` (step) → `order-status` (attribute): set
- `Ship` (step) → `order-status` (attribute): set
- `Ship` (step) → `Refund` (step): Execute failure

Example: [examples/python/dex_examples/products/order-processing/order_processing_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/products/order-processing/order_processing_flow.py)

Dex records the inputs, outputs, and errors of every Step.
Operators can inspect each order and, after a fix, rerun it from an earlier Step (time travel).

| Problem                 | Database and queues                                 | Dex Flow                                                            |
| ----------------------- | --------------------------------------------------- | ------------------------------------------------------------------- |
| Crash between two steps | The buyer can be charged while the item never ships | Dex records each Step result and the move to the next Step together |
| Several workers         | You build row claiming or rely on queue delivery    | Dex sends each task to one of your Workers                          |

### When requirements change

Each new requirement adds parts to the queue design.
In Dex it adds at most one Step or RPC and changes the Ship Step.

Refund the buyer after an hour of ship failures: database and queues add a refund-requests queue and refund consumers, and the ship consumers record the first failure and give up after an hour. Dex adds a Refund Step, and the Ship Step retries for one hour and then moves to the Refund Step. Wait for the seller's approval before shipping: database and queues add an approvals table and an approval endpoint, and the approval endpoint, not the charge consumer, publishes to ship-requests. Dex adds an approve RPC, and the Ship Step waits for a message on the seller Channel. Remind the seller: database and queues add a reminder cron job that scans the approvals table for orders that still wait. Dex adds no part: the Ship Step also waits for a Timer, and when it fires, the Step sends a reminder and waits again. In total, database and queues add 5 new parts, and Dex adds 2.

[Why Dex?](https://docs.superdurable.io/intro/what-is-dex.md) shows this Flow as Dex code, and the [Quick Start](https://docs.superdurable.io/quick-start.md) runs it.
