skip to content

Two installed extensions each register a component for the same role — how do you make the outcome deterministic?

level: seniorimportance: should knowfreq 42%

answer

  1. single slot or accumulating list
  2. discovery order is not a contract
  3. declare precedence or relative order
  4. explicit registration beats install order
  5. assert the resolved graph at startup

basics

~20 s

Decide the winner explicitly instead of inheriting it from discovery order: declare precedence or a relative order, or register the intended component yourself and exclude the other. Then assert the resolved component in a startup test so an upgrade cannot reshuffle it.

solid answer

~50 s

First work out whether the role is **single-slot** (one implementation serves it, so one registration must win) or **accumulating** (every registration joins a list, so order sets sequence rather than a winner) — the fixes differ. For a single slot, stop relying on whichever unit happened to install last: use the framework's **precedence or ordering declaration**, or take the decision out of the extensions entirely by registering the component you want yourself and excluding the unit you do not. For an accumulating role, declare relative order ("after this one", "before that one") rather than depending on discovery, which is not a contract and shifts when a build's dependency set changes. Finally, **assert the resolved graph in a startup test**: which implementation serves the role, or the order of the list. That is the only part of the fix an upgrade cannot quietly undo.

go deeper

for a junior

Understand that two installed units can both want to provide the same thing, and that the framework needs a rule to pick one rather than leaving it to chance.

for a middle

Explain the difference between a single-slot role, where one registration wins, and an accumulating role, where order sets the sequence, and say which fix belongs to each.

for a senior

Show how you make the outcome explicit under production pressure: precedence or your own registration plus an exclusion, and a startup assertion so a dependency upgrade cannot reshuffle the winner.

for a principal

Consider whether the platform should allow competing units at all: a house policy that one capability has one owning unit removes the class of bug, at the cost of a slower path for teams that need an alternative.

## First, classify the role Before fixing anything, decide which of two shapes you are in — the whole diagnosis follows from it. | Role shape | What a second registration means | What order controls | |---|---|---| | Single slot | One registration must win and the other is dropped or shadowed | Which implementation is in force | | Accumulating | Both are kept and the framework holds a list | The sequence in which they run | A symptom that changes *what happens* points at a single slot; a symptom that changes *the order things happen in* points at an accumulating role. Engineers who skip this step apply a precedence fix to a list problem, or a list fix to a slot problem, and conclude the framework is unpredictable. ## Why order is decided by accident in the first place Installable units are frequently **discovered** rather than named — the framework finds what is present in the build and applies each one. Discovery order is a by-product of how the build lays things out. It is stable enough to look like a rule during development and unstable enough to change on a dependency upgrade, a build-tool change, or a different packaging of the same artifact. Treating it as a contract is the root mistake behind "it works on my machine". The second contributor is that not every unit is polite. A well-built unit registers **only if the role is still empty**, so an explicit choice wins automatically. A unit that registers unconditionally will overwrite or shadow whatever came before, and two such units make the last one home the winner. ## The fixes, strongest first 1. **Take the decision yourself.** Register the implementation you want in your own startup code and exclude the unit that would compete. The graph is then explicit, and no ordering rule has to be remembered by the next reader. 2. **Declare precedence.** Where the framework supports a priority or ranking for registrations in one slot, set it on the one that should win. This keeps both units installed and states the intent in one place. 3. **Declare a relative order.** For accumulating roles, state "this runs after that" rather than a global number where you can: relative constraints keep holding when a third unit arrives, and absolute numbers collide. 4. **Drop one side.** If two units genuinely provide the same capability, having both is usually an accident of two packages arriving for overlapping reasons. Removing one is the cleanest resolution available. Avoid the pseudo-fixes: renaming a unit or a class so it sorts earlier, installing a unit twice hoping the second call wins, or reordering the entries in the build file. All three encode a rule nobody promised you. ## Make it stay fixed - A **startup test** that boots the application and asserts which implementation serves the role — for an accumulating role, assert the resolved sequence, not just membership. - A **behavioural check** where the difference is user-visible, because that is the assertion that keeps meaning something after a refactor. - A note at the exclusion or precedence site naming the other unit. The conflict will look arbitrary to the next reader without it. ## Related failure modes - **Both registrations present, one dead.** The losing component still gets built and may still hold resources or open connections, so the conflict costs more than confusion. - **Different winners in different environments.** A unit whose conditions depend on the environment can lose locally and win in production, producing a bug that does not reproduce. - **A silent flip on upgrade.** Neither unit changed, but the discovery order did; the symptom appears in a release whose diff contains nothing relevant. - **Double-installed hooks.** For accumulating roles, the conflict is often not a winner at all but the same behaviour applied twice — a cost paid on every request, and sometimes a correctness bug when the behaviour is not idempotent. ## The principle Any wiring outcome that depends on discovery order is an outcome nobody chose. The job is to move it from the build's incidental layout into an explicit statement — a registration you wrote, a precedence you declared, or a unit you removed — and then to hold it there with an assertion.

  • Why can a conflict like this appear in a release whose own diff changed nothing relevant?
    Because the winner was never chosen by your code. A dependency upgrade, a build-tool change or different packaging can reorder unit discovery, flipping which registration lands last in a single-slot role. The source looks identical because the decision lived in the build's layout, not in the source.
  • For an accumulating role, why prefer relative ordering over absolute priority numbers?
    Relative constraints state the requirement itself — this must see the request before that one — and keep holding when a third unit is added. Absolute numbers encode the requirement indirectly, collide as units accumulate, and force everyone to know the whole scale to insert one entry safely.
  • Is the losing registration in a single-slot conflict harmless?
    Not necessarily. Depending on the framework, the losing component may still be constructed, holding memory, connections or background work while nothing routes to it. It also confuses diagnosis: the component appears in a registration dump though it never serves a request.

saying these in an interview costs you the question

  • Treats discovery order as a stable, documented contract
  • Renames a class or unit so it sorts earlier
  • Installs a unit twice hoping the second call wins
  • Never distinguishes a single-slot role from an accumulating one
  • Fixes the order by editing the build file's dependency order
  • Assumes the losing registration costs nothing at runtime