Dependency Inversion Principle and DDD
Dependency Inversion Principle (DIP) is a technique used in Object Oriented Programing paradigm. The pattern was surfaced by Robert Martin (Uncle Bob) around 1994. Dependency Inversion is one of the five principles in SOLID. Before digging into DIP, it is good to understand what are Dependency Injection (DI) and Inversion of Control (IoC). Engineers tend to misinterpret DIP, DI and IoC as the same. DI and IoC are used in tandem with DIP.
As per Brett L. Schuchert (text)1, “DI is about how one object knows about another, dependent object.” When an object needs to send an SMS after some business operation, it refers to an object capable of sending SMS by passing a message. So the process of referencing the SMS processing object is via DI. A prominent library in Java used this pattern is Spring. Spring offloads the means of injecting dependencies out of the core business logic of the application. In java, the new operation for the dependent object is managed and injected to the code by Spring. Dependency injection significantly improves code comprehension and testability of code. Spring profiles can be used to inject dependencies based on the context where the application runs. For example, the application can behave differently, while in unit testing, testing, per-production or production.
IoC is about changing the object which initiates the message. Typically, a message is sent from a dependent object to the independent object. In IoC, the message is being sent by a separate object to the dependent object. As an example, see how the Observer Pattern uses the IoC principle. The independent object (Subject) initiates the message to the dependent object (Observer). Another example is how Spring container uses IoC principle to inject dependencies. Developers annotate the bean specification(s) (Observer) in the domain object, and IoC container injects the dependency at runtime (Subject) to the domain object.
DIP is about changing the direction of source code dependency of dependent module on a independent module(s), while keep the call flow from dependent to independent. For example, an application service needing data from DB should not depend on Data Access Service (ReadRepository) of the lower layer. Instead, the application service should depend on abstraction at the same level. I.e. an Interface of Data Access Service in same Application Service. The DIP would use DI to inject the real implementation Data Access Service at the runtime without creating direct source code dependency. One can use an IoC container to ease DI process. Bob Martins SOILD principle trainings covers more about the DIP. He explains when call flow and source code dependency are directing to same side those programs become fragile, rigid and none reusable. With the help of polymorphism one can invert the direction of source code control.
Layered Architecture isolates the expression of the domain model and the business logic, and eliminate any dependency on infrastructure, user interface, or even application logic that is not business logic.
Now let’s see what Domain-Driven Design (DDD) is. DDD tries to isolate the core domain of a system into a model using a key set of abstractions like Entities, Value Objects, Aggregates, Events, Domain Services, and Bounded Contexts. The purpose of isolation of domain is to build a Ubiquitous Language between the developer and the domain expert. DDD tries to minimise the mismatch of domain understanding between a developer and a domain expert while removing the misunderstanding on software model by the same two experts. The software model here refers to the Domain Model. It is import to notice that a Domain Model does not include any none functional aspects of a system. Removal of non-functional aspects improves the ability to write more functional unit tests with ease.
Applying DIP and Layered Architecture to DDD
This is an object diagram of an application where it processes Earliest Arrival Time (ETA) of delivery packages periodically. While processing the ETA, it updates the delivery ETA in DB if the ETA is still in the future.
The above object diagram has two layers. Infrastructure which is the lower layer. Application and Domain are in a higher layer. The initial request comes to EarlierArrivalTimeService.storePresentETAs()
method. This method is dependent on the same layer objects LatestETAGatewayRepository
and PresentETARepository
Interfaces. The implementation of these interfaces are in lower layer objects ETAGatewayRepository
and ETAORMRepository
. These two objects are injected at runtime to EarlierArrivalTimeService
object using DI. To see how DIP is used in this example, one must recognize that lower layer ETAGatewayRepository
and ETAORMRepository
objects are now dependent on the higher layerLatestETAGatewayRepository
and PresentETARepository
Interfaces.
So what is the real use of Layered Architecture and DIP in DDD? The Layered Architecture and DIP enables to isolate the core domain of a system. DI and IoC container play a supporting role. In the above example, the core domain is simple. See below to understand how Layered Architecture and DIP is used to isolate the core domain requirements from none domain aspects of the application.
- Find the new ETA read time. This is the time that user invokes
EarlierArrivalTimeService.storePresentETAs()method.
It is part of the user input and is not part of the domain model. - Use the read time to read the latest ETAs. The implementation is in the infrastructure layer
ETAGatewayRepository
object. The abstraction ofETAGatewayRepository
the objectLatestETAGatewayRepository
is defined in Domain Model. So the lower layer dependent on the higher layer as per DIP. This means the ETA object is defined as a Domain Entity in Domain Model rather than a POJO in Infrastructure Layer. This isolates any business logic of ETA core domain to ETA Domain Entity. I.e. precisely toisPast()
- Check whether an ETA arrival time is in the past. The implementation
isPast()
does this validation within the ETA Domain Entity and in isolation. - Filter out the past ETAs. The implementation
presentETAs(List<ETA>):List<ETA>
is in the ETA Domain Service. This an ETA core domain requirement to filter the past ETA from the recently found ETA list. This requirement too is isolated inETADomainService
. - Save the present ETAs. The implementation is in the infrastructure layer
ETAORMRepository
object. The abstraction ofETAORMRepository
objectPresentETARepository
is defined in Domain Model. So the lower layer dependent on the higher layer as per DIP. This means, the ETADomainService the object is defined as a Domain Service in Domain Model rather than the method in Application ServiceEarlierArrivalTimeService
. This isolate any business logic of core domain objectETADomainService
. I.e. precisely topresentETAs(List<ETA>):List<ETA>
The use of DIP, Layered Architecture and DI for Unit Tests
As discussed earlier when DIP and Layered Architecture is used in an application the core domain is isolated into testable units like Entities, Value Objects and Domain Services. In the example application above, the developer can independently test,
- ETA.isPast()
ETADomainService.presentETAs(List<ETA>):List<ETA>
The DIP and Layered Architecture, further removes the core domain requirement from infrastructure code like ETAGatewayRepository
and ETAORMRepository.
The developer can avoid any duplicate unit tests by making the infrastructure code as a boilerplate.
Further, there are no core domain requirements in EarliestArrivalTimeService
. The unit test for EarliestArrivalTimeService
would be minimal and it will check only the flow of the request based on test dependencies inject via DI.
Foot Notes
- This text covers more of the original topic but in detail.