Fundamentals 13 min read

When Constructors Do Too Much Work: Symptoms, Risks, and Refactoring Techniques

The article explains why having constructors perform excessive work is a design flaw, describes how to spot this problem through code symptoms, discusses its impact on testability and maintainability, and provides concrete refactoring strategies such as dependency injection, factories, and builder patterns.

Continuous Delivery 2.0
Continuous Delivery 2.0
Continuous Delivery 2.0
When Constructors Do Too Much Work: Symptoms, Risks, and Refactoring Techniques

Key points of this issue:

1. A constructor that does too much work is considered a code smell.

2. Why is it a flaw?

3. How to identify and fix it?

4. Code examples before and after the fix.

Defect: Constructors doing too much work

In many constructors you will see activities such as creating/initializing collaborator objects, communicating with other services, and setting up internal state. This makes it hard to isolate the class for unit testing because the collaborator instantiation blocks test setup.

If a constructor does too much, it hinders the instantiation of the class or its collaborators in test cases.

Signals emitted by this defect:

New keywords or static method calls appear in the constructor or field declarations.

The constructor does more than simple field assignment.

After the constructor finishes, the object is not fully initialized (note the init method).

Control‑flow statements (conditionals or loops) are present.

Multiple classes are constructed inside the constructor without using a factory or builder.

Additional initialization blocks or methods are used.

Why is this a problem?

The constructor must instantiate and initialize its collaborators, which leads to inflexible and tightly‑coupled designs that are hard to test. It violates the Single Responsibility Principle because the constructor mixes object creation with business logic.

Testing such constructors is difficult: the test must execute all the work the constructor performs, including external resources like files, network services, or databases, which often go untested.

Subclassing and overriding tests become harder when the constructor delegates to overridable methods; using subclass‑based test tricks should be a last resort.

The constructor also forces collaborators to be bound together, preventing the use of test doubles for heavy dependencies such as a real MySqlRepository when a lightweight mock is desired.

It erases a “seam” – a place where the code can be split and dependencies substituted – making it impossible to replace new ClassA() calls with test doubles.

Even if a constructor is written solely for testing, the production constructor remains problematic, and similar constructors in other classes will still be hard to test.

Bottom line

Ask yourself: "Is it difficult to isolate or replace the collaborators when creating a test double?" If the answer is yes, the constructor is doing too much work.

When writing code, always consider how hard it will be to test the constructor.

How to identify this defect

Any new keyword that creates an object you would like to replace with a test double.

Any static method call (static calls cannot be mocked or injected).

Any conditional or loop logic inside the constructor.

When reviewing code, ask: "How will I test this?" If the answer is not obvious, treat it as a warning sign.

Note

Value objects (e.g., LinkedList , HashMap , User , EmailAddress , CreditCard ) are usually acceptable in constructors because they are simple, state‑focused, and do not reference services.

How to fix the defect

Do not create collaborators inside the constructor; inject them instead.

Move the responsibility of constructing and initializing the object graph to another class, such as a builder or factory, and pass the collaborators to the constructor.

Examples (Java):

Use a Provider<YourObject> with Guice to supply constructor parameters, eliminating the need for manual initialization.

Apply manual dependency injection by using a factory or builder that creates the object graph, keeping new usage out of business‑logic classes.

As a last resort, provide an init(...) method that can be called after construction, though this is discouraged.

Before‑and‑after example

The following diagrams illustrate how mixing object topology creation with logic in a constructor makes testing difficult, and how extracting the creation into a separate builder or factory resolves the issue.

Problems highlighted:

Inline object instantiation in class members suffers the same issues as constructors.

If a class like Kitchen performs expensive operations (file/database access), it becomes hard to replace with a test double.

The design becomes fragile because you cannot substitute Kitchen or Bedroom implementations polymorphically.

When the collaborator is a value object (e.g., a simple list or map), inline creation is fine; the risk lies with service objects that should be injected.

Conclusion

Refactor constructors to receive collaborators via dependency injection, factories, or builders, and avoid embedding substantial logic or object creation inside them.

Unit TestingDependency InjectionRefactoringdesign principlesConstructorCode Smell
Continuous Delivery 2.0
Written by

Continuous Delivery 2.0

Tech and case studies on organizational management, team management, and engineering efficiency

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.