In my current project, we have multiple bounded contexts, each of which is implemented as a self-contained system. One of the architectural guardrails that was set in place at the beginning is the mandate that all systems use a ports and adapters architecture—separating business logic (use cases) from technical concerns (adapters like controllers) through defined interfaces (ports).
In order to enforce this mandate, we have some fitness functions implemented as ArchUnit tests. These tests find quite a few possible violations. For example, it will fail if a class from the domain depends on an adapter, port, or use case. However, the tests don’t prevent controllers from depending on domain interfaces. This flexibility, combined with delivery pressure, led to business logic accumulating in controllers. Not out of bad intentions or incompetence, but because the typical forces of delivery pushed us to choose immediate over long-term velocity.
Quite a while back, I started to follow the Scout Rule or Campsite Rule in my daily work: Leave a place better than you found it. As a software developer, this means leaving a code base better than you found it. This does not mean that you need to spend many hours each week on dedicated improvement tasks. Instead, the idea is to make it a habit to perform tiny improvements each day. These can take only 10 minutes or so and should be done along the way, for example while implementing a new feature.
A Real-World Example: Refactoring Towards Clean Architecture
In my role working on data products, I need to make sure that all the facts of what happened businesswise in our systems are made available. To this end, I sometimes need make the operative systems publish new domain events to our message broker. This is why, a few weeks ago, I needed to make some changes to this system I mentioned.
As I looked at where the publishing of the new domain event needs to happen, I realised that it was a controller. At first, I simply added the logic for publishing the domain event there. Then I stepped back and realised that this is actually far too much business logic in the controller. So I did a small refactoring: I introduced a new interface for an inbound port and created a use case class implementing this port interface. I then moved all the business logic from the controller method to the new use case class. And that was my Scout Rule work for that day.
When Past Cleanups Pay Off
A couple of weeks later, we learned about a new requirement: In certain cases, it should not be necessary to have an employee use the web interface to manually trigger a certain action. Rather, this action should be triggered automatically under those specific conditions. As we did a refinement, it turned out that this new requirement was trivial to implement for two reasons:
Firstly, the action to be triggered automatically was exactly the one whose logic I had moved from the controller to a dedicated use case a few weeks earlier in my habit of cleaning up along the way.
Secondly, the condition under which this action should be triggered automatically was already covered by a domain event that is being published if that condition is true. I added that domain event a few months ago, not because it was needed by the operational systems (it was not) but because I needed it to implement another data product.
It was a pleasant surprise to see that my Scout Rule habit combined with a domain event existing so far purely for reporting reasons will allow us to implement this new requirement with minimal changes to our code base. Adding the event consumer will be trivial. It’s just another adapter calling the same use case. What was a manual web UI action became automated event-driven behaviour, with no code duplication between the controller and event consumer, and no architectural changes needed.
The Compound Effect of Continuous Architecture Work
Practising the Scout Rule is all well and good, but it’s moments like these where you realise that daily continuous architecture work can really make a difference and lead to exponential improvements in the long term. It also demonstrated that the benefits of ports and adapters are not just theoretical, if you actually implement this architecture stringently. As my co-worker Fabian said with joy: “Now everything falls into place!”