Ambient context is data a piece of code reads without being handed it as an argument — it’s just there, in scope, by virtue of where and when the code runs. The current user, the tenant/account, a request id, the locale. The blanket advice “globals are bad” is right about application-wide mutable state and wrong as a universal rule: request- or thread-scoped ambient context is a legitimate, sometimes superior tool — for the right kind of concern. The whole question is which concern.
Why plain globals are bad
A true global (mutable, process-wide, shared across requests) earns its bad reputation:
- Hidden coupling. A function that reads a global depends on something its signature doesn’t mention. You can’t tell what it needs by looking at how it’s called — the same “control flow the call site can’t see” problem behind callbacks.
- Untestable in isolation. The global must be set up and torn down around every test, and a leak between tests causes spooky order-dependent failures.
- Concurrency hazard. Process-wide mutable state shared by concurrent requests is a data race waiting to happen.
What ambient context fixes
Scope is the pivot. Rails’ CurrentAttributes (Current.user, Current.account)
is thread/request-scoped and reset between requests, not application-wide —
which defuses the concurrency hazard and most of the test-leak hazard (the
framework clears it per request). Every ecosystem has this tool:
- .NET —
HttpContext,AsyncLocal<T>, scoped DI services. - Java —
ThreadLocal, SLF4J’s MDC (Mapped Diagnostic Context for logging), request-scoped beans. - Rails —
ActiveSupport::CurrentAttributes.
The value it buys, in Manrubia’s words:
The declarative callback approach with
Current.usercaptures this whole idea in a way that an explicit controller invocation doesn’t.
The test: is the concern secondary and orthogonal?
This is the line that makes the tool safe or dangerous. Ambient context is appropriate for data that is cross-cutting metadata, orthogonal to the operation’s actual logic — and wrong for data that is a genuine input to that logic.
The author’s framing for “who created this project”:
It’s more like an audit trait unrelated to what’s involved in creating a project, so it’s good to discharge the controller from knowing about it.
Sorting things out:
- Belongs in ambient context (secondary, orthogonal, present on everything):
current user for auditing, tenant/account,
request_id,user_agent,ip_address, locale, timezone. Threading these through every method signature would be pure noise that buries the real arguments. - Does not belong (primary domain input): the entity you’re operating on, the amount being charged, the order being submitted. If the method’s core logic branches on the value, it’s an argument — make it explicit, where the call site and a test can both see it.
A quick heuristic: would passing it explicitly be absurd boilerplate, or essential clarity? Locale on every method is absurd; the order being cancelled is essential. When you can’t tell, lean explicit.
The sharp knife
Manrubia’s own caveat is the right amount of fear:
You can put yourself in a deep hole if you misuse or abuse them. But, in our experience, there are scenarios where they are the best alternative at hand.
The deep hole is letting ambient context become an implicit argument to domain
logic. Then you have the same two-blind-spots failure as a hidden callback: the
call site can’t see that the method secretly reads Current.account, and the
method can’t see which call sites have set it (a background job, a console session,
or a test that forgot). The discipline is to keep ambient context out of the
decisions and confined to the metadata.
My read
Coming from .NET, Current is just HttpContext.Current / AsyncLocal<T> with a
nicer API, and I’ve watched that exact tool go both ways: excellent for stamping
request_id onto every log line, miserable when someone reaches into
HttpContext deep inside a domain service to fish out the current user and branch
on their role. The reframing I’m keeping: globals aren’t bad — process-wide
mutable state is bad, and ambient context smuggled into domain decisions is bad.
Request-scoped metadata that’s orthogonal to the logic is a clean way to not
pollute every signature, and the DI alternative
(injecting a scoped “current user” service) is the same idea with the wiring made
explicit. This is the cross-cutting-concern half of the vanilla-Rails sharp-knives
argument: legitimate when wielded knowingly for secondary concerns.
Related Concepts
- Constraints over Conventions — the same “two blind spots” hazard when context becomes implicit input
- Coupling — hidden dependencies are coupling the signature doesn’t reveal
- Inversion of Control — scoped DI as the explicit-wiring alternative
- Encapsulation — don’t let domain logic reach into ambient state
- Vanilla Rails vs. the Sharp Parts — “sharp knives” for secondary, orthogonal concerns