Multi-parent scopes
A scope is not limited to a strict tree. It can have several visible parents, forming a directed acyclic graph (DAG). This is what lets an object live at the intersection of several contexts.
A scope at a crossroads
A player inside a game lives at the intersection of two contexts — the game and the player's global presence:
record RootScope() {}
record Player(String name) {}
record GameScope(String name) {}
record GamePlayerScope(Player player, Game game) {}
Scope<RootScope> root = new Scope<>(new RootScope());
Scope<Player> globalPlayerScope = new Scope<>(new Player("Ada"));
globalPlayerScope.ownedBy(root);
Scope<GameScope> gameScope = new Scope<>(new GameScope("Arena"));
gameScope.ownedBy(root);
Scope<GamePlayerScope> gamePlayerScope =
new Scope<>(new GamePlayerScope(new Player("Ada"), game));
gamePlayerScope.ownedBy(gameScope); // owned by the game
gamePlayerScope.ownedBy(globalPlayerScope); // and visible from the global player scopeA class created in gamePlayerScope can see:
- the player-in-game local values;
- the
gameScopevalues; - the global player-scope values;
- the
rootvalues, visible through either parent.
The graph is a diamond — two visibility paths converge on one child:
┌─ root ─┐
└────────┘
▲ ▲
│ │
ownedBy │ │ ownedBy
│ │
┌──────────┘ └──────────┐
┌─ globalPlayerScope ─┐ ┌─ gameScope ─┐
│ │ │ │
│ Player = "Ada" │ │ GameScope │
│ │ │ │
└─────────────────────┘ └─────────────┘
▲ ▲
│ │
│ ownedBy ownedBy │
│ │
└──────────┐ ┌──────────┘
┌─ gamePlayerScope ──┐
│ │
│ Player, Game │ sees BOTH parents
│ │
└────────────────────┘
▲
gamePlayerScope.get(...)Lookup from gamePlayerScope fans out along every visible edge, so it reaches gameScope, globalPlayerScope, and root beyond them.
This is powerful for cross-cutting contexts: player + game, request + tenant, job + user, session + module.
ownedByties lifetime and visibility. When you want a parent to be visible without owning the child (or being owned by it), useweakRefinstead — see ownership vs visibility.
The ambiguity problem
With several visible parents, two of them can provide the same key. Resolving a single value is then ambiguous:
english.seed(Message.class, new Message("Hello"));
french.seed(Message.class, new Message("Bonjour"));
quebec.ownedBy(english);
quebec.ownedBy(french);
quebec.get(Message.class); // AmbiguousBeanExceptionNeither parent is "nearer" — both sit one hop away on separate branches, so there is no single nearest definition to pick:
┌─ english ──────────┐ ┌─ french ─────────────┐
│ │ │ │
│ Message = "Hello" │ │ Message = "Bonjour" │
│ │ │ │
└────────────────────┘ └──────────────────────┘
▲ ▲
│ │
ownedBy │ │ ownedBy
│ │
└────────────┐ ┌──────────┘
┌─ quebec ──┐
│ │
│ (none) │ get(Message) → two equally-near
│ │ matches → AmbiguousBeanException
└───────────┘Three ways to resolve it:
1. Shadow it locally — define the key nearer, so it wins:
quebec.seed(Message.class, new Message("Salut"));
quebec.get(Message.class); // "Salut"2. Qualify each side with a Key:
english.seed(Key.of(Message.class, "en"), new Message("Hello"));
french.seed(Key.of(Message.class, "fr"), new Message("Bonjour"));3. Ask for all of them instead of one:
for (Provider<Message> message : quebec.providers(Message.class).get()) {
System.out.println(message.get().text());
}NEAREST vs DEEP
Provider lookup walks the visible parents using a Scope.Collect strategy.
By default, Scope uses NEAREST: on each visible branch, the search stops as soon as a definition is found. This is the lexical shadowing model — the nearest definition wins.
scope.providers(Message.class); // NEAREST (default)
scope.providers(Message.class, Scope.Collect.NEAREST);DEEP keeps walking past a definition to also collect providers higher up:
scope.providers(Message.class, Scope.Collect.DEEP);Use NEAREST for normal resolution. Use DEEP when the caller wants to inspect or aggregate every visible contribution of a type across the whole graph — for example, collecting all registered handlers from every enclosing scope. (DEEP is also how the container itself merges extension hooks across the scope chain.)
Next: Extension hooks.