# Traces Are Not Your Business Logic

> Distributed traces track how your system processed a single request — not what your customers did over time. Confusing the two leads to poorly instrumented systems.

Source: https://last9.io/blog/traces-are-not-your-business-logic/

A team migrating to microservices instruments their checkout flow with [OpenTelemetry](https://last9.io/guides/opentelemetry/what-is-opentelemetry/). A few weeks in, a product manager opens a ticket: "Can we use traces to show a customer's full order history?" Another engineer asks: "I want a trace that shows all transactions for a given account."

The short answer is: that's not what a trace is. And confusing the two leads to poorly instrumented systems and misplaced expectations.

## What Business Logic Looks Like

Your business logic is a domain-level view of data and workflows. Think of it as what the product team cares about — the "what happened from the user's perspective."

**Order History (Business View)**

- Order #1042 -- placed Jan 3
- Order #1098 -- placed Jan 18
- Order #1134 -- placed Feb 2
- Order #1201 -- placed Feb 15

**Transaction History (Business View)**

- Debit $45.00 -- Jan 3
- Refund $45.00 -- Jan 10
- Debit $120.00 -- Jan 18
- Credit $10.00 -- Feb 1

This data lives in your database, stitched together by `user_id`, account references, timestamps. It spans days, weeks, or months. It's queried via SQL or your application's data layer — not captured by your [observability pipeline](https://last9.io/blog/modern-observability-system/).

## What a Trace Actually Is

A [distributed trace](https://last9.io/blog/distributed-tracing-an-advanced-guide/) tracks a **single request** as it flows through your system — from the moment an HTTP request hits your API gateway, to every downstream service call, database query, and cache lookup it triggers, until a response is returned.

A trace is scoped to one logical operation. It spans milliseconds to seconds, not days. Here's what a trace looks like for a single `POST /orders` request:

```
POST /orders  [trace_id: a3f9c2d1]                         0ms --> 245ms
|-- auth-service: ValidateToken                            2ms -->  18ms
|   └-- redis: GET session:user_882                        3ms -->   6ms
|-- inventory-service: CheckStock                         20ms -->  89ms
|   └-- postgres: SELECT products WHERE sku IN (...)      22ms -->  85ms
|-- orders-service: CreateOrder                           91ms --> 190ms
|   |-- postgres: INSERT INTO orders ...                  93ms --> 130ms
|   └-- kafka: Publish order.created                     132ms --> 188ms
└-- notifications-service: SendConfirmation              192ms --> 244ms
```

Every [span](https://last9.io/blog/opentelemetry-spans-events/) in that trace represents a single unit of work — a service call, a DB query, a message publish. Notice what's *not* there: the customer's past orders, their account balance, their full transaction history. That's intentional.

**A trace answers: "What did my system do to process this one request, and how long did each step take?"** It does not answer "what has this customer done over time?"

## The Structural Difference

Consider what happens when a user clicks "View Order History" in your app. Your frontend calls `GET /users/882/orders`. Here's the trace for *that* request:

```
GET /users/882/orders  [trace_id: b7e1a094]                 0ms --> 67ms
|-- auth-service: ValidateToken                             1ms -->  9ms
└-- orders-service: ListOrders                             11ms --> 66ms
    |-- postgres: SELECT * FROM orders WHERE user_id=882   13ms --> 60ms
    └-- redis: SET cache:user_882_orders                   62ms --> 65ms
```

The trace captures the *execution path* of fetching those orders — not the orders themselves. The data returned (the four orders from January and February) is a payload. It passes through the trace, but the trace is concerned with infrastructure behavior: how long the DB query took, whether the cache was written, which services were called.

**Key distinction:** Your order history is domain data. A trace is [operational telemetry](https://last9.io/blog/observability-vs-telemetry-vs-monitoring/). One tells you *what a user did*. The other tells you *how your software responded to a request*.

## Does Custom Instrumentation with `order_id` Help?

Yes — and it's absolutely worth doing. But Understand *what it unlocks* and *what it still won't give you*.

### What it does help with

If you attach `order.id = 1201` to your spans, you can search your tracing backend and pull up every trace that touched that order. So if a customer says "my order 1201 is stuck," you can see the full execution trail — the payment service timed out at 2:31 PM, the fulfillment service retried twice, the Kafka publish failed on the third attempt.

You can also correlate across services. If `order_id` is [propagated through your trace context](https://last9.io/blog/opentelemetry-context-propagation/), a span in the inventory service, the payment service, and the notification service all share that same attribute — giving you the entire distributed call graph for that order's lifecycle.

```javascript
// Instrumenting a span with business context
const span = tracer.startSpan("orders.create");
span.setAttributes({
  "user.id": 882,
  "order.id": 1201,
  "order.item_count": 3,
  "order.total_usd": 89.97,
});
```

Following [OTel semantic conventions](https://opentelemetry.io/docs/concepts/semantic-conventions/) for your attribute names keeps this instrumentation consistent across services and teams.

### What it still won't give you

It won't reconstruct your **business workflow** automatically. If a customer places an order, then calls support, then gets a refund — those are three separate traces (possibly hours or days apart), each with their own `trace_id`. Your observability backend stores them as isolated execution records.

If you query `user.id = 882`, here's what you'd actually see:

```
trace b7e1a094  GET /users/882/orders    67ms    Feb 15, 14:32:11
trace a3f9c2d1  POST /orders            245ms    Feb 15, 14:31:58
trace 9c44f812  GET /users/882/orders    54ms    Feb  2, 09:10:04
trace 2d71b309  POST /orders            312ms    Feb  2, 09:09:51
...
```

You're looking at request-level execution records, each isolated. Stitching these into a coherent order journey requires a different layer — your application database, a data warehouse, or an event log.

There's another practical constraint: trace data is typically sampled and has a short retention window (7-30 days in most setups). Head-based or tail-based [sampling](https://opentelemetry.io/docs/concepts/sampling/) means you may not even have every trace for a given `order_id`. Traces are not a substitute for durable business records — they were never designed to be.

### The pragmatic answer

Think of `order_id` on spans as a **debugging correlation key**, not a business history mechanism. It bridges the gap between "something went wrong" and "here's exactly which request, which service, and which line of execution caused it for this specific order."

The moment you need to answer "show me the full lifecycle of order 1201 from placement to delivery," you still want your application database or event log. Traces give you the low-level execution detail when something in that lifecycle breaks.

## The Right Tool for Each Job

| Question                                      | Tool                                                                                                        |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Why did this request take 800ms?              | Distributed trace                                                                                           |
| Which service caused this 500 error?          | Distributed trace                                                                                           |
| What's P99 latency of our checkout flow?      | [Trace-derived metrics](https://last9.io/blog/convert-opentelemetry-traces-to-metrics-using-spanconnector/) |
| Show me all orders for user 882               | Application DB / data warehouse                                                                             |
| What's the full lifecycle of order 1201?      | Event log / audit trail                                                                                     |
| Why did order 1201's payment fail at 2:31 PM? | Trace filtered by `order.id = 1201`                                                                         |

## The Takeaway

When you instrument your application with distributed tracing, you're capturing the execution behavior of your infrastructure for individual request lifecycles. Business-level continuity — an order's journey, a transaction's lifecycle across days — is domain data that belongs in your application layer.

The two can and should complement each other: attach business identifiers as [span attributes](https://last9.io/blog/otel-naming-best-practices/) so you can correlate traces to business events. But don't expect traces to replace your database-driven business views. They serve fundamentally different purposes, at different scopes, answering different questions.

_Traces tell you how your system behaved. Your database tells you what happened in your domain. Build both — and know which one to reach for._

---

If your team is working through these instrumentation decisions — figuring out which attributes to attach, how to correlate traces to business events without drowning in cardinality — [Last9 can help](https://last9.io/schedule-demo/). We handle high-cardinality trace data natively, so you can tag spans with `order_id`, `user_id`, or whatever your domain needs without worrying about cost explosions.
