From Big Ball of Mud to Modulith Part 2: Introducing Ports and Adapters architecture

In Part 1, we laid the foundation for a vertical architecture of our still muddy monolith. We are now able to enforce rules between PHP namespaces with deptrac.

But we can only enforce which namespaces talk to other namespaces; we don’t have a defined way yet. Let’s assume each namespace is a bounded context. Then other namespaces or bounded contexts should not simply be allowed to call every class of a bounded context, right? They should only be allowed to talk to the bounded context via a defined interface, or in Ports and Adapters speak, a driving port.

Ports and Adapters defines the inside and the outside of a hexagon (in fact the hexagon is arbitrary here): The inside contains the business logic and rules, entities, events, and so on. The boundary is usually a Bounded Context. The outside of the hexagon is technical infrastructure.

The left side, the “driving side”, is the inbound side—everything that talks to the hexagon. That could be your application’s REST or GraphQL API, or in monoliths, other bounded contexts. The right side is the “driven” side—everything the hexagon needs to work, for example databases, APIs, or in a monolith, other bounded contexts.

Step 1: Extract “driving” port interface in the Payment namespace

Let’s do this with our Payment from the example in the first post. We don’t want PaymentGateway::charge() to be called directly from outside the Payment namespace, but only via a defined interface.

Let’s call the interface Payment\DrivingPort\ForPaymentUseCase - which indicates it’s for one specific use-case:

Step 2: Adapt deptrac to enforce the rule

First, let’s create a new layer in deptrac.yaml called Payment_DrivingPorts.

1
2
3
4
- name: Payment_DrivingPorts
collectors:
- type: classLike
value: DeptracPortsAdaptersSample\\Payment\\DrivingPort\\.*

Next, we have to adapt the existing Payment layer, so that it excludes the Payment_DrivingPorts, otherwise the definition would be ambiguous.

1
2
3
4
5
6
7
8
9
- name: Payment
collectors:
- type: bool
must:
- type: classLike
value: DeptracPortsAdaptersSample\\Payment\\.*
must_not:
- type: layer
value: Payment_DrivingPorts

Finally, we need to adapt the “allowed layers”, so that the Order layer (in our case: Bounded Context) is allowed to call the Payment layer/bounded context via its driving ports:

1
2
3
4
5
Order:
- Infrastructure
- Payment_DrivingPorts # allow the Order domain to call Payment Ports
Payment:
- Payment_DrivingPorts # necessary to allow implementing the interface

Running deptrac will produce an error now (as we have not adapted the code yet):

1
2
3
4
5
6
7
-------------------------- ---------------------------------------------------------------------------------------------------------------------- 
Reason Order
-------------------------- ----------------------------------------------------------------------------------------------------------------------
DependsOnDisallowedLayer DeptracPortsAdaptersSample\Order\OrderProcessor must not depend on DeptracPortsAdaptersSample\Payment\PaymentGateway
You are depending on token that is a part of a layer that you are not allowed to depend on. (Payment)
/var/www/html/src/Order/OrderProcessor.php:10
-------------------------- ----------------------------------------------------------------------------------------------------------------------

Step 3: Allowing existing violations, but no new ones

If you’re trying to introduce this architecture into an existing piece of software, you will probably get a lot of violations at this step. But don’t give up—Deptrac can generate a baseline file with known violations:

1
2
3
$ docker compose run --rm php vendor/bin/deptrac analyze --formatter=baseline

Baseline dumped to /var/www/html/deptrac.baseline.yaml

The generated baseline looks like this:

1
2
3
4
deptrac:
skip_violations:
DeptracPortsAdaptersSample\Order\OrderProcessor:
- DeptracPortsAdaptersSample\Payment\PaymentGateway

We need to import the baseline into the deptrac.yaml:

1
2
imports:
- deptrac.baseline.yaml

The output looks like:

1
2
3
4
5
6
7
8
9
10
-------------------- ----- 
Report
-------------------- -----
Violations 0
Skipped violations 1
Uncovered 0
Allowed 3
Warnings 0
Errors 0
-------------------- -----

The advantage here is that existing violations are allowed and documented, but new violations will be detected (esp. when running deptrac as part of the CI workflow).

Step 4: Adapt code to fit into the architectural style

But let’s also fix our known violation now:

The driving port PHP interface looks straightforward

1
2
3
4
5
6
namespace DeptracPortsAdaptersSample\Payment\DrivingPort;

interface ForPaymentUseCase
{
public function charge(): void;
}

The calling code needs to be adapted, so that it uses the driving port interface:

1
2
3
4
5
6
7
8
class OrderProcessor
{
public function processOrder(ForPaymentUseCase $forPayment, Logger $logger): void
{
$forPayment->charge();
$logger->log();
}
}

Voilà, the calling code (OrderProcessor) is now decoupled from the inner workings of the Payment namespace.

(Note: The actual wiring is left out here, that’s usually done by a DI container.)

Let’s run deptrac again:

1
2
3
4
5
---------------------------------------------------------------------------------------------------------------------------------------------- 
Errors
----------------------------------------------------------------------------------------------------------------------------------------------
Skipped violation "DeptracPortsAdaptersSample\Payment\PaymentGateway" for "DeptracPortsAdaptersSample\Order\OrderProcessor" was not matched.
----------------------------------------------------------------------------------------------------------------------------------------------

Great! Deptrac complains that there is a skipped violation that does not exist (anymore, as we just fixed it). So let’s generate a new baseline deptrac analyze --formatter=baseline. For this small example, it results in an empty baseline. In real-world projects we would have at least removed one violation and have made this software a little better (aka the boyscout rule: We left the camping ground a bit cleaner than we found it.)

Like what you read?

You can hire me or make a donation via PayPal!

From Big Ball of Mud to Modulith: Introducing and keeping a clean architecture in PHP with Deptrac

While Deptrac is commonly used to enforce horizontal boundaries within PHP projects, it can also help define and maintain vertical boundaries (such as Bounded Contexts in DDD speak, or Quanta) in PHP monoliths.

This capability is especially valuable when implementing the Hexagonal aka. Ports and Adapters architecture, ensuring clear separation of concerns and robust modularity in your PHP applications.

This three-part series will cover:

Read More

Like what you read?

You can hire me or make a donation via PayPal!

Solutions to the Lost Update Problem

The Lost Update Problem is a common issue in concurrent systems, where two transactions read the same data, modify it, and write it back to the database. The second transaction will overwrite the changes made by the first transaction, causing the first transaction’s changes to be lost.

This is especially problematic in cases where money in a bank account is involved, as it can lead to inconsistencies in the account balance. A rather dramatic example of this is the Flexcoin bankruptcy.

Read More

Like what you read?

You can hire me or make a donation via PayPal!

Review-Driven Testing: Enhancing Code Quality, Upskilling Developers, and Promoting Testing Culture

In the world of software development, ensuring the quality and reliability of code is paramount. One popular approach to achieving this goal is Test-Driven Development (TDD), where developers write tests before writing the actual code. TDD is also my default way of approaching software development.

However, I’ve started in many environments situations where code changes are made without accompanying automated tests. In such cases, I am using a method I call “Review-Driven Testing”, which helps not only to improve code quality and upskill developers, but also to promote a testing culture within development teams.

What is Review-Driven Testing?

Review-Driven Testing is a practical approach to adding automated tests to code changes that were initially developed without them. While traditional TDD focuses on writing tests before writing code, Review-Driven Testing involves writing tests after the code has been developed and submitted for review. It can be seamlessly used in teams that are not used to writing automated tests by default (yet).

Read More

Like what you read?

You can hire me or make a donation via PayPal!

Easier Code reviews with Git Colored Move Diffs

Code reviews can be very time-consuming. Especially if you are reviewing a lot of code changes. Often, code changes are simple refactorings, such as extracting code into methods/functions or moving code around. These changes are more or less irrelevant for the review, since they usually don’t change the behavior of the code. As a reviewer, one still wants to make sure that code hasn’t been changed during the code move.

Read More

Like what you read?

You can hire me or make a donation via PayPal!

Upgrade CDK/Cloudformation-managed AWS Aurora Serverless v1 (MySQL 5.7) cluster to Serverless v2 (MySQL 8.0) with minimal downtime

Since AWS is going to sunset / auto-upgrade Aurora Serverless v1 in December 2024, it’s time to upgrade to Aurora Serverless v2. Upgrading to Serverless v2 MySQL instances also means upgrading from MySQL 5.7 to MySQL 8.0 under the hood.

This article describes how to adapt the AWS provided upgrade path to work with a CDK / CloudFormation infrastructure-as-code project, which requires a few more steps which I call the “CDK/CloudFormation retain-and-import dance“.

Read More

Like what you read?

You can hire me or make a donation via PayPal!

Zero-downtime upgrade from AWS Aurora 2 (MySQL 5.7) to version 3 (MySQL 8.0) with the CDK and Aurora Blue/Green deployments

This article demonstrates how to upgrade an CDK (or Cloudformation)-managed AWS Aurora cluster from MySQL 5.7 compatible AWS Aurora 2 to MySQL compatible Aurora 3 engine without any - ok, I lied - one minute of downtime. The process utilizes the blue/green deployment feature of Aurora. It also shows how CDK/CloudFormation stacks are brought back in sync with the upgraded Aurora clusters.

Read More

Like what you read?

You can hire me or make a donation via PayPal!

Automated MySQL RDS/Aurora initialization with CDK, Fargate and the MySQL CLI

A recurring task when provisioning databases is to initialize them with initial users, stored procedures, and/or tables.

In the CDK world, there is currently no native support for this, but there are some workarounds. AWS has a blog post about this, but it’s using a Lambda function, and a bunch of custom code which seemed like too much operational overhead for me.

Read More

Like what you read?

You can hire me or make a donation via PayPal!