Keep Documentation up-to-date with Deptrac
Every day, in every software project, I structure code in a certain way. There are always reasons why the code is structured in that way. One reason might be that the team agreed to follow a certain design pattern. Another reason might be that certain performance characteristics need to be met.
Architecture Decisions
In many software projects that I worked on, the reasons why code is structured in a certain way are not immediately visible. Having the reasons visible would give developers context and a better understanding of the code. This is helpful with:
- on-boarding of new developers
- aligning a team on architecture decisions
- takeing reasons into consideration when changing software
- keeping long-term goals in mind
At TicketSwap we started documenting these reasons in a format called ADR (Architecture Decision Records). These are lightweight documents which give a reason and context to an architecture decision. Documentation is great, but it has a risk of getting outdated. How can we help ourselves to ensure ADRs are updated?
One helpful tool in the PHP community is sensiolabs-de/deptrac.
Deptrac
This CLI tool allows you to define layers and code structure rules in YAML. It can validate that these rules are always met by integrating a validation step into the CI pipeline.
Not all architecture decisions are about code structure. But for the ones that are, the tool can help to ensure that the underlying ADR is up to date and implemented. Using Deptrac saves time in code reviews because I don’t have to check that the ADRs are followed.
Example: Domain Separation
Within the context of a financial service, we defined separate domains like customers, payments, payouts, refunds etc. We want to keep each domain isolated to battle complexity as the service grows.
What does this mean in practice? Let’s only focus on the customer
domain for this example. Because we use the command and event pattern, this means for us:
- customer controllers can only use customer commands
- customer command handlers handle only customer commands and only publish customer events
- customer subscribers only handle customer events
In Deptrac you can define controllers, command handlers, commands, etc. as layers
. I’m using className
collectors to assign all classes to a layer that match a certain regular expression. In a file domain_customer.yml
all layers relevant for this particular customer domain separation are defined like this:
layers:
- name: CustomerController
collectors:
- type: className
regex: .*Controller\\.*Customer\\.*
- name: CustomerCommandHandler
collectors:
- type: className
regex: .*Domain\\Handler\\Customer.*
...
The dependencies between layers are defined in a ruleset
. This describes which layer may depend on which other layers. In this example, it looks like this:
ruleset:
CustomerController:
- CustomerCommand
- CustomerEntity CustomerCommandHandler:
- CustomerCommand
- CustomerEvent
- CustomerEntity
...
When running the deptrac analyze
command all code files are scanned and validated with the layers and ruleset previously defined.
deptrac analyze domain_customer.yml --formatter-graphviz-display=0
If we would raise an event from the payout
domain in a customer command handler, we would get an error like this:
Domain\Handler\Customer\SavePayoutMethodHandler::12 must not depend on Domain\Event\Payout\PayoutAvailableEvent (CustomerCommandHandler on NotCustomerEvent)
The PayoutAvailableEvent
is not a customer event but we raise it in a customer command handler.
This is one example for a quite technical reason for structuring code in a certain way. But this doesn’t always have to be this way.
Another example that has business reasons is in the context of this financial service and integration with third-parties. We want be able to anticipate on changes in regulations or payment platforms during the lifetime of the service. To make sure we can easily switch out payment platforms, we have a ruleset that validates that the payment platform code is isolated.
Continuous Integration
After defining rulesets, it is important to make sure that they are validated all the time. For that, we added Deptrac to our CI pipeline running on CircleCI. To install Deptrac I added this step in the CircleCI configuration:
- run:
name: Setup Deptrac
command: |
set -exu
if [ ! -f ./vendor/bin/deptrac ]; then
curl -LS http://get.sensiolabs.de/deptrac.phar -o ./vendor/bin/deptrac
chmod +x ./api/vendor/bin/deptrac
fi
And to validate, I added a validation step which runs each ruleset:
- run:
name: 'Validate Deptrac'
command: |
set -exu
vendor/bin/deptrac analyze tests/Deptrac/domain_cart.yml --formatter-graphviz-display=0
vendor/bin/deptrac analyze tests/Deptrac/domain_customer.yml --formatter-graphviz-display=0
vendor/bin/deptrac analyze tests/Deptrac/platform.yml --formatter-graphviz-display=0
...
Summary
Documenting why a decision was made is helpful for new developers and alignment in teams. Architecture decisions that are about code structure can be validated automatically using Deptrac. This frees you from having to check this manually. It’s also a great way to make sure that the documentation matches reality. The Deptrac rulesets can either directly link to an ADR or explain the reasoning in a comment.
Are you interested in working with me? Join us at TicketSwap.