A team migrating to microservices instruments their checkout flow with 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.
What a Trace Actually Is
A distributed trace 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 --> 244msEvery span 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 --> 65msThe 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. 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 it's important to 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, 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.
// 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 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 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 |
| 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 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. 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.
