When an order goes wrong in a multi-provider system, your first question is always the same: what happened? Most teams answer by reading logs scattered across multiple systems. Oruve answers by replaying the entire order history from the event log.

The state problem

Traditional systems track current state: the order is in state X, the PSP confirmed at timestamp Y, the tracking number is Z. This works when everything goes smoothly. When something breaks, you hit a wall.

An order gets stuck. You check the database. It says "awaiting_shipment" but it's been there for three days. Now what?

  • Was the order actually sent to the PSP?
  • Did the PSP accept it?
  • Did we get a response back?
  • Why didn't the state advance?
  • When exactly did it fail, and why?

You dig into logs. But the logs are scattered: some in your application server, some in the PSP adapter, some in the message queue, some in the PSP's system that you don't control. And they're noisy. You have to reconstruct a narrative from fragments.

Current state is a lie. It's always missing the context of how you got here. — Martin Fowler, Event Sourcing

What if we logged events instead?

Instead of storing state, store every change that led to that state. Every transition is an immutable event with a timestamp, context, and outcome.

An order's event stream looks like:

  • order.received (timestamp: 14:22:10, source: shopify)
  • order.normalized (timestamp: 14:22:10, canonical_form: {...})
  • routing.started (timestamp: 14:22:10, candidates: [psp_a, psp_b, psp_c])
  • psp_a.queried (timestamp: 14:22:10, response_time_ms: 45)
  • psp_b.queried (timestamp: 14:22:11, response_time_ms: 120, status: timeout)
  • psp_c.queried (timestamp: 14:22:11, response_time_ms: 78)
  • routing.decided (timestamp: 14:22:11, winner: psp_c, score: 87)
  • order.submitted_to_psp (timestamp: 14:22:11, psp_id: psp_c, request_id: abc-123)
  • psp_c.accepted (timestamp: 14:22:15, psp_order_id: xyz-789, due_date: 2026-05-22)
  • order.confirmed (timestamp: 14:22:15, eta: 2026-05-22)

Now when you ask "what happened?" you don't have a mystery. You have a complete timeline. You see where it went, when it got there, and why.

Why replay matters

Once you have immutable events, you can do something traditional systems can't: replay history to rebuild any historical state.

Imagine you discovered a bug in your capacity calculation. You deployed a fix. But did the fix matter? Were orders mis-routed before the fix? By how much?

With event sourcing, you can replay all orders from last Tuesday with the new logic and compare routing decisions. You get a before-and-after comparison with real data. You can measure impact. You can decide whether to re-route historical orders or leave them as-is.

Or imagine a PSP claims we never sent them an order. You can replay the event stream, show them the exact request we transmitted, and prove the order was delivered.

Or you want to add a new routing strategy without affecting live orders. You replay historical orders with the new strategy, validate the results, then migrate forward.

Debugging becomes answerable

When someone asks "why did this order route to Provider X instead of Provider Y?" you don't guess. You walk them through the event stream:

  • Here's when the request came in (14:22:10)
  • Here's which providers were candidates at that moment
  • Here's how long each provider took to respond
  • Here's the score each received (capacity: 95, SLA: 92, cost: 88)
  • Provider X won because it had the best combined score, which was 8 points higher than Provider Y

It's no longer "the algorithm decided." It's "here are the inputs, here's the calculation, here's why."

That transparency matters when you're debugging operational issues, when you're auditing decisions for compliance, or when a customer wants to understand their fulfillment flow.

How we built it

Oruve logs every event to a dedicated events table with high-write, low-read access patterns. The schema is simple:

  • order_id — which order this event belongs to
  • event_type — "order.received", "routing.decided", etc.
  • timestamp — when it happened (server time)
  • actor — what system triggered it (intake, routing engine, adapter, webhook handler)
  • context — JSON blob with all relevant details
  • duration_ms — how long this step took (if applicable)

The interesting part is that we don't derive state from querying the events table on every read. We project events into a separate state table for fast reads. The state table is always just a cached projection of the event stream. If it ever gets corrupted or out of sync, we rebuild it by replaying events.

It adds complexity, but it unlocks something you can't get from traditional systems: complete observability into what happened, when, and why.

That's the kind of visibility we think print infrastructure should have.