The Dependency Rule
About this...
A software architecture rule that specifies the relationship between layers, namely that an inner layer should never rely on anything from an outer layer.
The Dependency Rule
In Uncle Bob's book, he describes the dependency rule.
That rule specifies that something declared in an outer circle must not be mentioned in the code by an inner circle.
That means that code dependencies can only point inwards.
For example, Domain Layer code can't depend on Infrastructure Layer code, Infrastructure Layer Code can depend on Domain Layer code (because it goes inwards).
In other diagrams, there are many more layers. The rule still applies.
Why does this matter?
- We avoid circular dependencies and keep code testable. The Acylic Dependency Rule describes this phenomenon in more detail.
How does this work?
- By both being conscious about how we hook up dependencies, making sure we only refer to lower layers directly, and using the Dependency Inversion principle from the SOLID Principles to refer to abstractions of upper layer concerns (instead of concrete implementations).
Layered Architecture Dependency Rules
Let's talk about some of the rules we need to follow when we layer our code this way.
Domain Layer
OK, let's map out some common scenarios.
Everything can depend on Domain Objects (entities, value objects, domain services) within the domain layer.
But Domain Objects can depend on exactly nothing outside the domain layer.
That's because the domain layer contains the highest level policy. Everything else depends on it.
The domain layer should be the most stable dependency and depend on classes from no other layer except itself in order to prevent cycles.
Application (Use Case) Layer
Use cases heavily rely on the only layer underneath it (domain) but also needs access to infrastructure layer things like repositories.
Try creating any use cases without access to persistence. Most of them need it. Take the CreateUserUseCase
from the "Functional Error Handling with Express.js and DDD" guide:
type Response = Either<
CreateUserError.UsernameTakenError |
CreateUserError.EmailInvalidError |
CreateUserError.AccountAlreadyExistsError |
CreateUserError.InsecurePasswordError
,
Result<any> // OK
>
class CreateUserUseCase implements UseCase<Request, Promise<Response>> {
private userRepo: UserRepo; // violation of the dependency rule
constructor (userRepo: UserRepo) {
this.userRepo = userRepo;
}
public async execute (request: Request): Promise<Response> {
const { username, email, password } = request;
const emailOrError: Either<CreateUserError.EmailInvalidError,
Result<Email>> = Email.create({ email })
if (emailOrError.isLeft()) {
return left(emailOrError.value);
}
const passwordOrError: Either<CreateUserError.InsecurePasswordError,
Result<Password>> = Password.create({ password });
if (passwordOrError.isLeft()) {
return left(passwordOrError.value);
}
try {
const [userByUsername, userByEmail] = await Promise.all([
this.userRepo.getUserByUsername(username),
this.userRepo.getUserByEmail(email),
])
const usernameTaken = !!userByUsername === true;
const accountCreated = !!userByEmail === true;
if (usernameTaken) {
return left(CreateUserError.UsernameTakenError.call(username));
}
if (accountCreated) {
return left(CreateUserError.EmailInvalidError.call(email));
}
return right(Result.ok())
} catch (err) {
return left(AppError.UnexpectedError.create(err))
}
}
}
The blaring problem is that the infrastructure layer is above the application layer. It would be a violation of the dependency rule to mention the name of anything in the infrastructure layer.
The way we fix that is dependency inversion of course.
Instead of the use case relying directly on the UserRepo
from the infrastructure layer, we can put an interface (depending on the methodology, people call this different things like port, abstraction) inbetween the two.
That changes the dependency relationship from this:
To this:
We've just unbroke the Dependency Rule violation that was there a second ago.
Ports and Adapters: This is a huge part of the "Ports & Adapters" methodology. Thinking this way means that there's another layer in between the application and infrastructure layer called the Adapter Layer. The Adapter Layer contains only ports (interfaces) that specifies for the Infrastructure Layer, how to create an adapter for the port, and specifies to the Application Layer, what to expect from an implementation of that port.
This kind of design is SOLID and enables us to respect the Object-Oriented Design principle stating to "always program to an abstraction, not a concretion."
Infrastructure Layer
The infrastructure layer, which primarily contains implementations of adapters to things like repositories, controllers, and integrations to services (like external APIs, Stripe, etc), can depend on any other layer below it.
For example, a controller (infra) will usually rely on a specific use case from the application layer.
class CreateUserController extends BaseController {
private useCase: CreateUserUseCase;
constructor (useCase: CreateUserUseCase) {
super();
this.useCase = useCase;
}
async executeImpl (): Promise<any> {
const { username, email, password } = this.req.body;
try {
const result = await this.useCase
.execute({ username, email, password });
if (result.isLeft()) {
const error = result.value;
switch (error.constructor) {
case CreateUserError.UsernameTakenError:
return this.conflict(error.getValue().message)
case CreateUserError.EmailInvalidError:
return this.clientError(error.getValue().message);
case CreateUserError.AccountAlreadyExistsError:
return this.conflict(error.getValue().message);
case CreateUserError.InsecurePasswordError:
return this.clientError(error.getValue().message);
default:
return this.fail(error.getValue().message);
}
} else {
return this.ok(this.res);
}
}
catch (err) {
return this.fail(err);
}
}
}
That's totally valid and follows the Dependency Rule.
Join 20000+ value-creating Software Essentialists getting actionable advice on how to master what matters each week. 🖖