ArchitectMind
  1. Home
  2. Tutorials
  3. Book 3 · Jakarta EE & Enterprise Java
  4. Volume II
  5. Dependency Management
Book 3 · Volume II — CDI, Dependency Injection & Interceptors · Topic 1.1

Dependency Management

Dependency, Graph, Tight coupling, Loose coupling

6 min readStage 1 · Foundations6 interview questions
On this page
  1. 1. · Concept & Definition
  2. 2. · Why It Matters
  3. 3. · How It Works
  4. 4. · Java / Jakarta EE Code Examples
  5. 5. · Configuration, Defaults & Diagnostics
  6. 6. · Production Usage Patterns
  7. 7. · Common Pitfalls & Misconceptions
  8. 8. · Best Practices
  9. 9. · Architect's Perspective & Trade-offs
  10. 10. · Interview Q&A

1· Concept & Definition

A dependency is any object, service or resource that a component needs to do its work — a repository, a payment gateway, a clock, a configuration value. Dependency management is the discipline of deciding how components obtain their dependencies, how those dependencies are connected into a graph, and how strongly components are coupled to the concrete implementations they use.

TermDefinition
DependencySomething a component uses to fulfil its responsibility
Dependent (client)The component that uses the dependency
Dependency graphDirected graph of 'uses' relationships between components
CouplingDegree to which a change in one component forces changes in another
Tight couplingClient knows and constructs concrete implementations
Loose couplingClient depends on an abstraction; implementation supplied from outside

2· Why It Matters

  • Coupling determines the cost of change: tightly coupled code must be edited and retested whenever a collaborator changes.
  • The dependency graph determines startup order, testability and the blast radius of failures.
  • Unmanaged dependencies lead to hidden global state, duplicated construction logic and untestable code.

3· How It Works

Dependency

Kind of dependencyExamplesTypical source
ServiceOrderRepository, PaymentGatewayApplication or container bean
Infrastructure resourceDataSource, JMS ConnectionFactory, HttpClientContainer-managed resource / producer
ConfigurationTimeouts, URLs, feature flagsMicroProfile Config, environment
Cross-cutting utilityClock, Logger, ID generatorProducer or library
Context informationCurrent user, tenant, requestScoped bean / security context

Graph

Components and their dependencies form a directed graph. Construction must proceed from the leaves (infrastructure without further dependencies) to the roots (entry points such as REST resources). A container computes this order automatically. Cycles are a design smell: with constructor injection they cannot be resolved at all; with proxies or field injection they may work but indicate responsibilities that should be split.

A typical dependency graph from entry point to infrastructure
Figure 1.1.1 — A typical dependency graph from entry point to infrastructure
Graph propertyImplication
DepthLong chains increase startup time and debugging effort
Fan-out of a nodeMany dependencies → class probably has too many responsibilities
Fan-in of a nodeWidely used components need stable interfaces
CyclesSplit responsibilities, introduce events or an intermediate abstraction

Tight coupling

A component is tightly coupled when it constructs its own dependencies with new, calls static factories or singletons (PaymentGateway.getInstance()), or reads configuration directly. The component then knows concrete classes, constructor arguments and configuration of its collaborators, and cannot be tested or reconfigured in isolation.

Loose coupling

With loose coupling, a component depends on an abstraction (interface) and receives an implementation from outside. It knows what it needs, not how it is built. Loose coupling enables substitution (tests, environments, vendors) and localises change to the component that owns a decision.

Tight coupling versus loose coupling through an abstraction
Figure 1.1.2 — Tight coupling versus loose coupling through an abstraction

4· Java / Jakarta EE Code Examples

Tight coupling — hard to test, hard to reconfigure
// Tightly coupled: construction, configuration and concrete types inside the client
public class ReportService {
    private final ReportRepository repo = new JdbcReportRepository(
            DriverManager.getConnection("jdbc:postgresql://db/reports", "app",
                    "secret"));
    private final Clock clock = Clock.systemUTC();
    // static singleton
    private final Mailer mailer = SmtpMailer.getInstance();
 
    public void sendDailyReport() { /* uses repo, clock, mailer */ }
}
Loose coupling — construction moved outside the class
// Loosely coupled: depends on abstractions, receives them from outside
public class ReportService {
    private final ReportRepository repo;
    private final Clock clock;
    private final Mailer mailer;
 
    public ReportService(ReportRepository repo, Clock clock, Mailer mailer) {
        this.repo = repo; this.clock = clock; this.mailer = mailer;
    }
    public void sendDailyReport() { /* same logic */ }
}
Building the graph by hand at a single composition root
// Manual wiring ("composition root") — what a container automates
DataSource ds        = createDataSource(config);
ReportRepository rep = new JdbcReportRepository(ds);
Mailer mailer        = new SmtpMailer(config.smtpHost());
ReportService svc    = new ReportService(rep, Clock.systemUTC(), mailer);

5· Configuration, Defaults & Diagnostics

Tool / signalWhat it reveals
Static analysis (ArchUnit, jdeps)Package/module dependencies, forbidden references, cycles
Count of constructor parametersHigh numbers (> 5–7) suggest too many responsibilities
new of service classes in business codeHidden tight coupling
Container deployment errorsUnsatisfied/ambiguous/cyclic dependencies detected at startup

6· Production Usage Patterns

  • Business services depend on interfaces for external systems (payments, email, storage).
  • Infrastructure implementations live in separate packages or modules (ports and adapters).
  • Architecture tests forbid domain packages from depending on infrastructure packages.

7· Common Pitfalls & Misconceptions

  • Interfaces for everything — abstraction without variation adds noise; abstract where substitution or isolation is needed.
  • Static singletons and service locators — hide dependencies and global state.
  • Cyclic dependencies masked by field injection.
  • God services with dozens of dependencies.

8· Best Practices

  • Depend on abstractions at architectural boundaries (I/O, external systems, time).
  • Keep dependency graphs shallow and acyclic.
  • Make dependencies explicit in constructors.
  • Enforce layering with architecture tests.

9· Architect's Perspective & Trade-offs

Coupling typeExampleAcceptable when
To stable library typesList, BigDecimal, Clock interfaceAlways
To own domain typesOrder, MoneyWithin the same bounded context
To infrastructure implementationsJdbcOrderRepositoryOnly in wiring/adapters
To external vendorsStripe SDK classesOnly behind an adapter interface

10· Interview Q&A

Q1Basic

What is a dependency in object-oriented design?

A dependency is anything a component needs from outside itself to do its job: another object such as a repository or a payment gateway, a resource such as a data source or a message queue, or a configuration value such as a timeout. For example, an OrderService that saves orders and charges cards depends on an OrderRepository and a PaymentGateway. How a component obtains its dependencies matters: if it creates them itself with new, it is tied to specific implementations; if it receives them from outside, through its constructor for instance, they can be replaced for testing or different environments.

Q2Intermediate

What is the difference between tight and loose coupling?

With tight coupling, a component refers directly to concrete implementations, for example by calling new JdbcOrderRepository() inside OrderService. Any change to that class, or a wish to use a different one in tests, forces changes to OrderService. With loose coupling, the component depends on an abstraction, such as an OrderRepository interface, and receives the implementation from outside, typically through constructor injection. The component no longer knows which implementation it uses, so implementations can change, be mocked in unit tests or vary by environment without touching it. Loose coupling has a cost in indirection, so apply it at meaningful boundaries.

Q3Intermediate

Why are cyclic dependencies problematic?

A cyclic dependency exists when A depends on B and B, directly or indirectly, depends on A. With constructor injection the cycle cannot be built at all, because neither object can be created first; containers either fail at start-up or fall back to field or setter injection, which hides the problem. More fundamentally, a cycle usually signals unclear responsibilities: two components that each need the other probably belong together, or share a third concept that should be extracted. Cycles also prevent testing, releasing or reusing either component on its own. Break them by extracting the shared part or introducing an interface or event.

Q4Senior Engineer

What is a composition root?

The composition root is the single place where the application's object graph is assembled: where concrete implementations are chosen and wired together. In a small application it can be the main method creating objects by hand; in Jakarta EE or Spring it is the container, configured by annotations and configuration classes. Keeping all construction there means business classes only declare what they need, usually through constructors, and never decide which implementation to use or how to create it. That keeps them easy to test and keeps environment-specific choices, such as a real or a fake payment gateway, in one place.

Q5Technical Architect

How would you detect and prevent harmful coupling in a large code base?

Make the intended dependency rules explicit and check them automatically. Architecture tests with ArchUnit can assert, for example, that domain packages never depend on web or persistence packages, and fail the build when someone breaks the rule. The Java module system or separate build modules can make illegal dependencies impossible to compile. Track signals of growing coupling, such as package cycles, classes with many constructor parameters and changes that ripple through many modules. At boundaries to external systems, require abstractions, such as a PaymentGateway interface, so vendor libraries do not spread through the code base. Review the rules as the architecture evolves.

Q6Solution Architect

How does dependency management relate to system-level architecture?

The same principles apply at every scale. Within a service, classes depend on interfaces rather than implementations; between modules, dependencies follow a deliberate direction towards stable core modules; between services, consumers depend on published API and event contracts rather than on another service's internals or database. Managing dependency direction consistently is what allows parts of the system to evolve, be tested and be deployed independently. For example, a payment service can change its implementation freely as long as its API contract holds. Poor dependency management at system level produces the same symptoms as in code: cycles, ripple effects and coordinated releases.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.