Recently I was in a discussion about what looked like a very simple feature.
“We just need to send an email when something fails.”
Fine.
Add some categories. Add totals. Add program metadata. Maybe a few conditional sections. Send it out.
Easy.
Except it wasn’t.
The more we unpacked it, the clearer it became that this was not an email. It was a report. And not just a report, but one that pulls information from multiple parts of the system and presents it as if it all lives in one place.
I have seen this pattern many times.
A PRD describes what someone wants to see. That is its job. It describes the surface.
But it quietly assumes structure underneath.
It assumes:
- The data is unified.
- The lifecycle is unified.
- One place understands what happened.
Real systems do not look like that.
In a distributed system, different layers see different realities.
One layer sees raw input.
Another sees transformed records.
Another sees historical context.
Another enforces rules.
They do not share the same semantics.
When a PRD blends all of that into one clean report, it is assuming there is a composition layer that understands all of it.
Sometimes that layer exists.
Often it does not.
And if you implement the PRD literally, you create it.
The email worker starts calling multiple services.
It pulls errors from one backend.
It enriches from another.
It merges states.
It normalizes categories.
It becomes the place where what happened is defined.
At that point you are not sending a notification.
You are building a reporting surface.
Now here is the uncomfortable part.
Most systems already have a composition layer for the UI. The frontend calls a gateway. The gateway orchestrates across services and returns a unified view.
Now the email worker has to do the same thing.

Two places composing the same report.
Two places to evolve.
Two places to test.
Two places where semantics drift.
None of this is in the PRD.
But it becomes real the second you start coding.
Now, experienced engineers will say:
“Fine. Extract the orchestration into a shared service. Make it a reporting module. Call it from both the gateway and the worker.”
Yes. That is a valid solution.
But that is not a small refactor.
That is a system decision.
You are introducing a canonical reporting layer that aggregates across services, defines lifecycle semantics, and owns the shape of the report. That layer becomes a contract.
The question is not whether we can build it.
The question is whether we are consciously choosing to build that layer, or whether we are backing into it because the PRD happened to describe a detailed template.
Those are two very different mindsets.
There is another angle here that matters.
Engineers should not jump directly from PRD to implementation.
They should dissect it.
They should map it to the actual shape of the system.
Sometimes when you do that, you realize the PRD is not describing one thing.
It is describing three.
Maybe one email should be sent when a file fails structural validation during ingestion.
Maybe another should be sent when the program limits are evaluated and a run breaches contract rules.
Maybe a third is not even part of ingestion at all, but an alert triggered by anomaly detection in the warehouse.
Those are three different layers.
Three different ownership boundaries.
Three different semantics.
Bundling them into one clean report might look nice in a document, but it may not reflect how the system is actually designed.
And that is fine.
Product describes value.
Engineering translates value into system shape.
Sometimes that translation means saying:
This is not one email. It is three lifecycle events.
Or:
If we want it to look like one report, we need to explicitly build a reporting layer that aggregates those events.
That is not resistance.
That is system thinking.
Think about chess for a second.
Sometimes the pawn structure locks. No breaks left. No tension. The board hardens. You can still move pieces, but you are not creating anything new. You are playing inside a structure you built earlier.
Architecture behaves the same way.
If you encode today’s internal layout directly into an external surface, you lock the structure. Later, when validation moves or ownership changes or layers split, you either break the contract or bend the system to preserve it.
That is the part I care about.
This is not anti-PRD.
It is not anti-detail.
It is about being deliberate.
If we need a cross-system reporting layer, great. Let’s build it intentionally.
What we should not do is let it quietly emerge inside a worker because the template looked harmless.
The question is not “How do we send this email?”
The question is “What are we actually building?”
That is the difference between shipping a feature and shaping a system.