When wiring a web framework's container, when do you register a dependency by type, by name, or with a factory?
answer
- how does the container tell candidates apart?
- type is the default request
- a name when two share a type
- a factory for non-service arguments
- ambiguity versus hidden edges
basics
~20 sRegister by type when one implementation satisfies an abstraction, so the parameter type is the request. Add a name when several instances of one type mean different things. Use a factory when construction needs non-service values or a wiring-time choice.
solid answer
~40 sRegistering **by type** binds an abstraction to one implementation, and any parameter of that type is filled without the consumer saying anything — the style to prefer while there is only one answer. Registering **by name** adds a discriminator because two instances of the same type are genuinely different things, such as a primary and a replica connection; the injection site must then state which it wants. A **factory** registration gives the container a function to call instead of a constructor, for the cases it cannot work out itself: non-service arguments read from settings, an implementation chosen at wiring time, or an instance produced by a builder or pool. Type registration risks ambiguity, names risk resolution-time typos, and factories hide graph edges inside a function body.
code
pseudocode · 15 lines# by type: one implementation satisfies the abstraction
register(PaymentGateway -> CardGateway)
# by name: two instances of one type mean different things
register("primary", DataSource, url = settings.primaryUrl)
register("replica", DataSource, url = settings.replicaUrl)
# by factory: construction needs values the container cannot invent
register(ReportBuilder, factory = (c) ->
ReportBuilder(clock = c.resolve(Clock),
pageSize = settings.reportPageSize))
# consumer states a name only where one is needed
class OrderHandler(gateway: PaymentGateway,
reads: DataSource named "replica")go deeper
Learn the default first: bind one implementation to one abstraction and let the parameter type do the asking. Recognise that a name is only needed when two instances share a type.
Explain what each style costs. Type registration risks ambiguity, a name pushes a wiring detail into the consumer and fails only at resolution, and a factory buys non-service arguments at the price of edges the container can no longer see.
Show judgment about where discriminators and factories accumulate. Be ready to explain how a mis-registered same-typed pair can start cleanly and serve wrong data, and how you would catch it before a deploy.
Treat registration style as a convention the whole codebase pays for. Decide where names are allowed, how they are defined, and how much of the graph may hide inside factories before validation stops being meaningful.
## Every registration answers three questions When you add a service to a container you are answering: **under what key** will it be found, **how** is it built, and **who chooses** when several candidates could satisfy the same key. The three common registration styles are three different answers to those questions, and mixing them badly is where wiring stops being mechanical. ## Registering by type The default style binds an **abstraction to one implementation**: requests for the abstraction resolve to that implementation, and any constructor parameter typed as the abstraction is filled with it. Nothing at the injection site has to say anything — the parameter's type *is* the request. This is the style to reach for whenever the answer to "which one?" is "there is only one". It keeps consumers free of wiring knowledge, which is the property that makes a graph easy to re-point later. Its failure mode is **ambiguity**: register two implementations under one abstraction and resolution has no rule to pick between them, so most containers fail rather than guess. ## Registering by name or key Sometimes two instances of the *same* type are genuinely different things: a connection to a primary store and one to a replica, an outbound client aimed at one partner and another aimed at a second. The type no longer identifies them, so registration adds a **discriminator** — a name, a key, a qualifier — and the injection site must state which one it wants. The tradeoff is explicit: you have bought the ability to have several, and paid by pushing a wiring detail into the consumer's declaration. A named binding is also the one style where a typo produces a missing binding at resolution rather than an error a compiler could have caught, so names are worth defining once as constants rather than spelling out at each site. ## Registering with a factory A factory registration hands the container a **function to call** instead of a constructor to invoke. Use it when the container cannot work out the arguments by itself: - construction needs **values that are not services** — a timeout, a base address, a feature flag read from settings; - the implementation is **chosen at wiring time** by inspecting settings or the environment; - the object is produced by a builder that must be configured step by step before it yields the instance; - the instance comes from somewhere else entirely — a pool, a cache, another library's entry point. A factory receives a handle to the container (or its own injected parameters) so it can resolve what it needs and then apply the non-service arguments itself. The cost is opacity: the edges of the graph now live inside a function body, so the container can no longer report them, and a graph-validation pass cannot see through them. ## Choosing between them | Style | Use when | Injection site says | Main risk | |---|---|---|---| | By type | Exactly one implementation satisfies the abstraction | Nothing — the type is the request | Ambiguity if a second is added | | By name or key | Several instances of one type are different things | The name it wants | Typos fail only at resolution | | By factory | Construction needs non-service values or a runtime choice | Nothing extra | Hidden edges, harder to validate | ## Registering several implementations on purpose A fourth case is worth separating from ambiguity: sometimes you really do want **all** of them. Many containers let several registrations share one key and inject the whole set as a collection, which is how pipelines of validators, notification channels or format writers are assembled. The consumer takes a collection rather than a single instance, and adding a behaviour becomes one registration with no consumer edit. Ordering across such a set is rarely defined by the container, so when order matters, express it in the registration or sort explicitly inside the consumer. ## The failures each style produces 1. **Missing binding** — nothing is registered under the requested key, or a name is misspelled. 2. **Ambiguous binding** — two recipes share a key that expects one answer. 3. **Silently wrong instance** — two same-typed services registered without discriminators, where the container happily resolves the last one registered; this is the dangerous one, because it starts successfully and misbehaves at run time. 4. **Unresolvable factory argument** — the factory asks the container for something that was never registered, which surfaces only when the factory runs. A practical rule: prefer **by type** until a second implementation genuinely exists, add a **discriminator** the moment two same-typed instances mean different things, and keep **factories** for the arguments a container cannot invent.
- What happens when two implementations are registered under one abstraction with no discriminator?The key now has two recipes and no rule to choose, so most containers refuse to resolve it and report an ambiguous binding. Some instead take the last registration to win, which is worse: the application starts and quietly uses the wrong instance. Either way the fix is a discriminator, or collapsing the two into one.
- Why does a factory registration weaken graph validation?Because the edges move inside a function body. A container derives dependencies from a constructor's parameters, but it cannot read what a factory will resolve when it runs. A binding the factory needs can therefore be missing while the graph still looks complete, and the failure appears only when that factory is first invoked.
- How do you inject several implementations of one abstraction deliberately?Register each under the shared key and have the consumer take a collection of that abstraction rather than a single instance. Pipelines of validators, notifiers or format writers use this, and adding one becomes a single registration. Ordering is rarely guaranteed across such a set, so encode it in the registration or sort inside the consumer.
saying these in an interview costs you the question
- Uses a factory for every service, hiding all graph edges from the container
- Registers two same-typed services and expects the container to guess correctly
- Thinks names are needed whenever an abstraction has more than one possible implementation
- Puts settings values into constructors by reading globals instead of passing them at registration
- Believes a collection injection and an ambiguous binding are the same mistake