My TypeScript Software Design & Architecture book just prelaunched! Check out solidbook.io.
Close

The Dependency Rule

Updated Sep 17th, 2019 / 4 min read

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.

In other diagrams, there are many more layers. The rule still applies.

That means that code can only point inwards.

Domain Layer code can't depend on Infrastructure Layer code.

But Infrastructure Layer Code can depend on Domain Layer code (because it goes inwards).

When we follow this rule, we're essentially following the Dependency Inversion rule from the SOLID Principles.

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:

modules/users/useCases/createUser/CreateUserUseCase.ts
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.

0 Comments

Be the first to leave a comment

Submit