Uniform Access: The Same Request, However the Answer Is Actually Made
A caller shouldn’t need to know what’s behind the interface. Only what they asked for.
A task to make a cup of coffee doesn’t specify a kettle. It doesn’t specify an espresso machine. It specifies an outcome. What’s actually in the kitchen decides how the task gets carried out. One kitchen has a hob and a kettle. Another has a coffee machine. Neither kitchen changes what the task means.
Refine the outcome and the task adapts without ever naming equipment. Fair Trade beans. Ground medium. Milk warmed to a specific temperature. The task still doesn’t say which espresso machine to use. It just says what the result needs to look like.
That’s the whole idea, before any of it touches software.
If you are not yet a member of medium.
Logical Task vs. Manifestation
A task, logically defined, is separate from how it actually gets carried out. This is the same logical form versus manifestation distinction introduced in Consistent Addressing. A logical form says what something is. A manifestation is one specific way of realising it.
Applied to work rather than to identity, the same task can be manifested many different ways depending on what’s available to do the work. Nothing about the task itself has to change for that to be true.
The Uniform Access Principle, Extended
Bertrand Meyer’s Uniform Access Principle made a narrow claim about a single value. A caller shouldn’t be able to tell whether that value is stored or computed on the fly. Both should look the same from the outside.
Extend that past one value to a whole request. A caller shouldn’t need to know whether the answer came from a live stream, a table in a warehouse, or a structure sitting in memory. The interface stays constant. What’s actually behind it doesn’t have to.
What a Request Actually Carries
A uniform request has two halves.
Context covers where, when, and who. Where is the environment the request is running in. When is the set of expectations attached to it, deadlines, cost limits, guidance for how trade-offs should be resolved. Who is the authority behind the request, the identity making it, and any chain of delegation involved.
Activity covers what. What state the system needs to be in before the task can run. What the task actually is. What should happen with the result once the task completes.
Together, Context and Activity are what this series called machine-interpretable intent back in the first piece. Not a request phrased loosely enough to need a human to interpret it. A request precise enough for a system to act on directly.
Get Stephen James Price’s stories in your inbox
Join Medium for free to get updates from this writer.
That request is what the Composer calls a demand. It goes to the Composer, covered earlier in this series, to resolve how the outcome behind it can actually be achieved. The Outcome portion of the request travels alongside it as an instruction for the very last step. Once the task has run, however many entity stores or processors that took, the result gets emitted in the exact shape the consumer asked for. A SQL query and a streaming query can both terminate in “hand me back a list of records,” even though nothing about how they ran was the same.
Continuation Across Tiers
A single request doesn’t have to resolve against one source for its entire lifetime. Historical data might sit in a database or a file. Recent data might only exist in a live stream. A request can start against one tier, and when that tier runs out of coverage, hand off to the next.
Making that handoff safe means passing forward a continuation instruction. Not just “move to the stream now,” but exactly where in the stream to pick up. An offset. A timestamp. Whatever anchor the next tier needs to carry on without a gap or a duplicate.
The direction isn’t fixed, either. A request could start against a file and hand off into a database once the file’s coverage runs out, carrying forward whatever continuation that tier needs instead. Same mechanism, reversed order.
None of this needs the consumer to know it happened. The first tier answers with a continuation attached. The next tier picks it up. What looks like one uninterrupted stream of results is actually two sources, stitched together behind the same interface the consumer is already using.
That’s the theory. Here’s what it looks like when it’s actually running.
One Aggregation, Two Engines
A weighted percentile calculation is a genuinely awkward piece of logic. It needs a running window of values, sorted, weighted, and re-evaluated as new data arrives.
Written for a batch job, it took the shape of an iterator over a Spark dataset. Written for the same purpose in a live system, it took the shape of a stream processor, reacting to each new value as it arrived. The general shape of the logic was the same in both. The actual library imported underneath it was different in each case.
Nobody rewrote the aggregation. They wrote it once, logically, and it got manifested twice.
Why It Matters
A market data feed into an algorithmic trading engine needed to work three different ways, behind one interface.
In unit tests and regression tests, the data came from a file. In production, it came from a live stream. On restart, it needed to replay from a Log-Structured Merge-Tree first, to recover state, before switching over to the live stream. That replay-then-switch is the continuation mechanism described above, playing out for real. The LSM-tree replay was the first tier. The live stream picked up exactly where the replay left off.
Three sources. One consumer, written once, that never needed to know which of the three it was actually talking to. The engine asked for market data. What supplied that data changed depending on context. The code asking for it didn’t.
With Uniform Access, all three sat behind the one interface. That meant the data layer could be proven correct in isolation. No need to stand up the fully assembled trading engine just to regression test how market data was being read.
Three benefits followed from that. Proving a change was correct got faster and cheaper. It happened at the data layer, not the full system. Developers working on the engine didn’t need to hold three data sources in their head at once. They wrote against one interface and trusted it. And reliability improved, because a variance in how data behaved surfaced at the data layer, in isolation, before it ever reached full assembly.
This is what makes the Composer and Uniform Access work as one pipeline, not two separate ideas. The Composer resolves how an outcome gets achieved. Uniform Access is the shape of the request going in, and the guarantee on the format coming out. A consumer never needs to know if the answer came from a stream, a table, or a structure in memory. They also never need to know what format it was natively produced in. They asked for records. Records are what they get.
Close
None of this holds together if the request and its answer can’t travel the same way, regardless of what’s actually fulfilling them. That’s the next piece in this series. The Envelope is the wire format a uniform request, and the outcome it produces, actually travels in.









