Maintain A Clean Architecture With Dependency Rules

Disentangle the dependencies between your packages.

Written by Reka Horvath on

Clean architecture

Packages, Dependencies, And Architecture

As soon as your application gets a bit bigger, you’ll probably divide it into subsystems. In Python, this usually means packages. How well the boundaries between those packages are defined and how the communication flows between them greatly influences the maintainability of your project.

In this blog post, we’re going to:

  1. Visualize the optimal flow of communication between packages.
  2. Phrase some rules (in English) about the dependencies between those packages based on the visual.
  3. Translate the rules into code, so that we can check how much our application follows the optimal architecture.

Following this example, you can create architecture rules for your own projects.

The Example API

We’re using a FastAPI application as an example. It stores its data in a PostgreSQL database via SQLAlchemy. It’s divided into 4 packages:

Visualizing the Communication Between Packages

How do these packages communicate with each other? And with the external libraries powering them?

This might take many different forms. One possible architecture is this:

clean architecture

But it can also look something like this:

various dependencies

You can make things even more complicated by introducing circular dependencies:

circular dependencies

While Python doesn’t allow circular dependencies between modules, it won’t stop you from introducing circular dependencies between packages.

The first architecture looks more simple. And this has some advantages:

Defining the Dependency Rules

Now, we have a picture of how these packages should communicate. Let’s define some rules to achieve this architecture. First, we’ll establish some rules on how the packages of our application communicate with each other:

Let’s turn to the external dependencies. We want to ensure that each of them is accessed only by the relevant package of our system:

Note that you probably don’t want to define such a rule for every single dependency. But for libraries that are a crucial part of the architecture, it’s a good idea to keep the interface clean.

Express the Dependency Rules as Code

So far, we’ve defined the dependency rules of our system: in a picture and in some sentences. Let’s see how it matches up with the reality of our system. 😏 For that, we’ll take the rules from the previous section and translate them into code.

For this translation, we’ll use the tool Sourcery Rules Generator It creates Sourcery’s custom rules in YAML format based on your input. You can install it from PyPI:

pip install sourcery-rules-generator

Setting Up Our Boundaries

Let’s start with the first rule:

No other package depends on api.

To create such a rule about dependencies, run the command:

sourcery-rules dependencies create

You’ll see:

Provide the parameters, so that the dependency rules can be generated:

screenshot: sourcery-rules dependencies create input parameters

Now, you’ll get an output with 2 rules in YAML format:

These rules detect if app.api or any of its subpackages gets imported within your project.

rules:
    - id: dependency-rules-app-api-import
      description: Do not import `app.api` in other packages
      pattern: import ..., ${module}, ...
      condition: module.matches_regex(r"^app\.api\b")
      paths:
          exclude:
              - app/api/
              - tests/
      tags:
          - architecture
          - dependencies
    - id: dependency-rules-app-api-from
      description: Do not import `app.api` in other packages
      pattern: from ${module} import ...
      condition: module.matches_regex(r"^app\.api\b")
      paths:
          exclude:
              - app/api/
              - tests/
      tags:
          - architecture
          - dependencies

What do these rules contain?

You can copy these rules into your projects .sourcery.yaml config file. For that it’s recommended to use the --plain option:

sourcery-rules dependencies create --plain

Now, you can run the sourcery review command of the Sourcery CLI to check for these rules:

sourcery review --enable architecture .

Further Rules for Internal Dependencies

Now, we can create similar Sourcery custom rules for our other internal dependency rules.

Only api depends core.

Again, we run the same command:

sourcery-rules dependencies create

This time, we fill both parameters:

screenshot: sourcery-rules dependencies create for core <- api

Again, 2 rules are created:

rules:
    - id: dependency-rules-app-core-import
      description: Only `app.api` should import `app.core`
      pattern: import ..., ${module}, ...
      condition: module.matches_regex(r"^app\.core\b")
      paths:
          exclude:
              - app/core/
              - tests/
              - app/api/
      tags:
          - architecture
          - dependencies
    - id: dependency-rules-app-core-from
      description: Only `app.api` should import `app.core`
      pattern: from ${module} import ...
      condition: module.matches_regex(r"^app\.core\b")
      paths:
          exclude:
              - app/core/
              - tests/
              - app/api/
      tags:
          - architecture
          - dependencies

You can also create similar rules ensuring that only app.core should import app.db.

Rules for External Dependencies

Let’s turn to the rules regarding the external dependencies:

We can create these rules the same way we did for the internal dependencies. This time, the package won’t be a package in our project, but an external dependency.

Only app.api imports FastAPI.

sourcery-rules dependencies create

This time, we fill both parameters:

screenshot sourcery-rules dependencies create fastapi and app.api

Again, the output shows 2 rules:

rules:
    - id: dependency-rules-fastapi-import
      description: Only `app.api` should import `fastapi`
      pattern: import ..., ${module}, ...
      condition: module.matches_regex(r"^fastapi\b")
      paths:
          exclude:
              - fastapi/
              - tests/
              - app/api/
      tags:
          - architecture
          - dependencies
    - id: dependency-rules-fastapi-from
      description: Only `app.api` should import `fastapi`
      pattern: from ${module} import ...
      condition: module.matches_regex(r"^fastapi\b")
      paths:
          exclude:
              - fastapi/
              - tests/
              - app/api/
      tags:
          - architecture
          - dependencies

You can create rules in the same way for the 2 other external dependencies as well:

Now, we have 12 Sourcery custom rules to express our 6 architecture rules. We can run all of them with the sourcery review command in the Sourcery CLI:

sourcery review --enable architecture .

Generalization: Create Architecture Rules in Your Project

Following the examples above, you can create rules for dependencies in your own projects. The recommended steps:

  1. Draw a diagram showing the optimal dependencies between your packages.
  2. Phrase some rules in a human language based on the diagram: Which package should depend on which?
  3. Translate the rules into code with Sourcery custom rules with Sourcery Rules Generator.

If you’re working on a huge application, don’t try to tackle the whole architecture at once. Pick a subset of packages and go through the 3 steps with this smaller scope.

You can also check out the how-to guide Establish Rules for Dependencies Between Your Packages in the Sourcery docs.

Have you created architecture rules for your system? Do you have any questions, thoughts on this? Let us know.

Reach out at hello@sourcery.ai or on Twitter @SourceryAI. Join the Sourcery Discord Community.