ThisCalculator

Everyday

Register a Concrete Class or Its Interface?

Updated · Standard formula, shown below

Register a Concrete Class or Its Interface?

Usually, register the interface when application code should depend on an abstraction, then map that interface to a concrete class in your dependency-injection container. Register the concrete class directly when it is an implementation detail used in one place or when no meaningful interface is needed; the right choice depends on how the type is consumed, not on a rule that every class must have an interface.

What a registration does

In a dependency-injection system, registration tells a container how to supply an object when code requests a type. A registration commonly identifies a service type, an implementation type, and sometimes a lifetime, such as one instance per request or a new instance each time. Exact terms and lifetime behavior vary by framework.

For example, an application might define an IEmailSender interface and a SmtpEmailSender class. Registering the interface-to-class mapping means a component that asks for IEmailSender can receive an instance of SmtpEmailSender. The component need not know which implementation the container chose.

Registering only SmtpEmailSender, by contrast, makes that concrete type the service that consumers request. Both approaches can construct and inject objects. Their difference is the dependency contract they expose and how easily the implementation can be changed.

Why register an interface

An interface is useful when it describes a capability that callers need without exposing implementation details. A class that sends messages may use an external mail service today and a test double or another provider elsewhere. If callers depend on IEmailSender, switching implementations can be localized to configuration rather than requiring changes throughout the application.

  • Reduce coupling: consumers rely on the operations in the interface, not on a particular class’s constructors or extra methods.
  • Support alternatives: a container can map the same service contract to different implementations in different environments or configurations.
  • Simplify isolated tests: tests can supply a fake implementation of the interface without constructing the real system or contacting an external service.
  • Clarify boundaries: an interface can express what a component is allowed to ask another component to do.

These benefits are real when an abstraction serves a purpose. An interface that merely repeats every method of one class, has no independent meaning, and is never substituted can add files and indirection without improving design.

When direct concrete registration is sensible

Register a concrete type directly when it is a straightforward service with one expected implementation and its callers are not meant to vary it independently. This can be appropriate for an internal helper, a framework adapter used in one narrow location, or a small application where introducing an interface would create ceremony rather than flexibility.

Concrete registration is also useful when the concrete type has meaningful behavior or configuration that callers intentionally need. However, injecting that type couples consumers to its public API. If it later becomes necessary to replace it, tests may need to construct more dependencies, and callers may need code changes.

Choosing a concrete registration does not mean abandoning dependency injection. The container can still manage construction and lifetime. The decision is about the type consumers request, not whether the object is created with new or by a container.

How to choose

Start with the consumer’s needs and the likely variation in the system. Ask what the code should depend on, rather than automatically creating an interface for every class.

  1. Identify the role. Is this a meaningful capability, such as storing documents or sending notifications, or simply an implementation detail?
  2. Look for real alternatives. Are there multiple implementations now, or a credible need for a test double, platform-specific behavior, or a different provider?
  3. Check the boundary. Does the dependency cross a module, application layer, or external-system boundary where callers should be insulated from implementation changes?
  4. Consider the cost. Will an interface make code easier to test and change, or will it exist only to mirror one class?
  5. Follow local conventions. Existing project patterns and framework guidance matter, especially where registration, decorators, scanning, or lifetime configuration are standardized.

A useful default is to expose abstractions at boundaries and use concrete classes for details behind those boundaries. For instance, an application service may depend on IOrderRepository, while the repository implementation depends on a concrete database client internally. There is no need to make every internal object an application-wide interface.

Common registration patterns and pitfalls

Many containers support a mapping such as “when asked for IClock, create SystemClock.” They may also permit direct registration of SystemClock. If both registrations are added separately, the container may create separate instances when the service is requested through each type. Whether that happens depends on the framework and registration method.

If the same object must be available through both an interface and its concrete type, configure both service types to resolve to the same instance or follow the container’s documented aliasing pattern. Do not assume two registrations automatically share an object. This matters for stateful services and for lifetimes such as singleton or scoped services.

  • Do not register an implementation that cannot satisfy the contract. The class must implement the interface and be constructible by the container.
  • Check lifetime compatibility. A long-lived service generally should not capture a shorter-lived dependency in a way the framework forbids or that causes stale state.
  • Avoid overbroad interfaces. A small, focused contract is easier to implement and test than one that gathers unrelated operations.
  • Do not confuse registration with substitution. An interface makes implementations interchangeable only when they honor the same expected behavior and callers do not rely on implementation-specific details.

Testing and maintainability trade-offs

Interfaces can make unit tests more controlled, but they are not the only way to test code. Depending on the language and tools, tests may use real lightweight implementations, in-memory components, delegates, or other seams. A test that replaces every dependency with a mock can become brittle if it verifies internal call details rather than observable behavior.

Concrete dependencies are not inherently untestable. A class that has clear inputs and deterministic behavior may be tested directly. The strongest reason to add an interface is usually an architectural boundary or a genuine need for multiple implementations—not merely the hope that a mock might be useful someday.

Also distinguish an interface from an abstract class. An interface generally specifies a contract without requiring shared instance state or implementation, while an abstract class can provide shared behavior and state. Language features differ, but either can serve as an abstraction; choose based on the relationship and capabilities the design needs.

Frequently Asked Questions

Should every class have an interface?
No. Add interfaces where they represent useful contracts, boundaries, or substitution points. For simple implementation details with one stable use, direct concrete dependencies are often clearer.

Can I register both an interface and its implementation?
Yes, if the container supports it and the design needs both service types. Configure the registrations carefully: separate registrations may create separate instances, depending on the framework.

Is registering an interface better for unit testing?
It can make it easier to supply a fake, especially around external services. But an interface alone does not guarantee good tests, and not every class needs a mockable contract.

Should consumers depend on an interface or the concrete class?
Prefer the interface when callers should be independent of implementation choices. Use the concrete class when its specific behavior is intentionally part of the dependency and extra abstraction offers no practical benefit.

Does an interface registration control object lifetime?
Not by itself. Lifetime is configured by the container’s registration options, and the exact behavior depends on the framework. Check its documentation and ensure related registrations behave as intended.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.