Refactor legacy application with DDD

Coding CEO
6 min readJun 13, 2021

I’ve spent a big part of my developer life refactoring applications. For whatever reason, it seems that I am good at this type of “challenges”. But in my current project, I decided to try something new: Domain-Driven Design. The result is much better than expected.

First, I will talk a bit about the software I am refactoring. I’m very sure most of you will find similar situations in your life.
The software is 7 years old made on PHP. In the beginning, the initial dev team was… let’s say “cheap”, so they made a lot of mistakes; no SOLID design, a lot of code in the controllers (files with 10.000 lines), and extremely poor quality.
After 3 years, the new CTO decided to start from scratch. They started with one small module, and after 6 months, only 10% of the job was done. So the refactoring was canceled.
Later on, the company was acquired, and a new team tried to rebuild the module again, this time on C#. After 6 months, was canceled again.

And this is the point where I join the team as Lead Dev, with the goal of making the application scalable and reliable, and looking for migration from Maria to PostgreSQL and C#.

For today’s example, I will use something that you will understand easily: Let’s say we have an eCommerce application with Customers, Orders, and Catalog. Our goal is to rebuild the application, migrate it to another database using another programming language.

I have always a couple of key points in my mind:

  • Rebuilding the application from scratch is not a failproof solution rarely. The abandon rate is too high, only if you reduce the scope to an MVP will work.
  • The Legacy Application is paying your salary, even if is a shit of software, is “working”. It will need to continue working, improving features, new requirements, etc. for example if there is a new Tax Law, you may have to rewrite part of the application. You can not wait for the new application to finish.

My approach for doing this kind of refactoring is to focus on the Business Model & Rules first. Once the business model is clean, the remaining will be migrated.

We will do it slice by slice, deploying in production legacy and new code simultaneously. And the most important thing; a flag determines which users will use the new version or the legacy one.

We will have one application running in 2 databases at the same time, with 2 different architectures, and different programming language.

It may seem a bit overkilling, but the benefits make it worthy:

  • Is failproof, you can roll back at any time.
  • You can check that the new code provides the same results as the legacy one.
  • You can deploy new code faster, no need to wait for a whole module to be refactored.

So, let’s start refactoring our eCommerce application applying DDD, in this case, will use CQRS that is closely linked to DDD.
The first thing is deciding which part of the application will be migrated first, in this example, we will migrate “Order”, which will go from “LegacyOrder” to “NewOrder”

  1. Introducing the command bus

In our code, we are going to locate were we create a LegacyOrder and were we write to any property, for example:

  • $order=new LegacyOrder();
  • $order->status=’SEND’;
  • $order->trackingNumber=$trackingNumber;

We are going to replace all this code with the Command Bus pattern, that initially we may wrongly create:

  • CreateOrderCommand
  • UpdateOrderStatusCommand

Although the first is correct, the second one is not SOLID, is not explicit, and is doing a lot of things in one command, so we will do:

  • SendOrderCommand
  • CancelOrderCommand
  • UpdateTrackingCommand

Now, we will create the Command Handlers. For each command, we will write (still in the legacy part) a handler that creates, send, cancel and update the tracking of the orders.

At this point, tests should still work, and everything should work as before.

What’s the objective of this?

  • Centralize all writing operations in the same place, without impacting the rest of the application.
  • Document the business: Commands are linked to “use cases”, identify the business legacy, this is better than having documentation. Is a “coded documentation”.

2. Introduce the NewOrder Entity

Let’s apply DDD, and create an Order Domain. In this case, the NewOrder will be persisted in another database, because we decided to migrate from Maria to PostgreSQL.

The new Domain will have also the same Commands and Handlers that the legacy part.

3. Parametrize de Command Bus to use the new part

It’s time to use the new code. Let’s parametrize that customers from one postal code will use the new code, so, when we call:

$commandBus->execute($sendOrderCommand);

The command bus will execute legacy or new handler depending on the configuration.

At this point, we will have some orders going to Maria and some orders going to PostgreSQL… but there are lot of code that needs the orders, for example, the order list in the Backoffice or “My Account” in the front.

4. Synchronizing data from NewOrder to LegacyOrder

We can do a similar approach when accessing Orders info using a Query Bus, but, legacy applications tend to do a lot of joins and complex queries that make it difficult to replace all queries with a Bus.

Depending on how the application is done, and the part of the application that we are refactoring we may decide on different strategies for querying data from the database: Clone data, or use the query bus.

In my case, we decided to synchronize the info between NewOrder and Legacy Order:

How we do it:

In DDD, your business logic is uncoupled from the infrastructure (like the database), the framework, or other parts of the application. So, to clone the data to the legacy Maria we have to do it in the infrastructure part of our DDD application because is not part of the business logic.

You can subscribe to the AfterSave event on your code that stores in PostgreSQL, that way is uncoupled.

Note: Don’t confuse infrastructure events with Domain Events, has nothing to do with each other. In theory, you can do the synchronization with Domain Events also, I do it sometimes, but, as they are normally asynchronous, can break easily the legacy application.

Let’s recap:

  • The legacy application works, tests pass, and we have our Business Model in DDD, clean, uncoupled.

5. Rebuilding the API and the Front.

Now is time to rebuild the API and the Front, and let’s start with “My Orders”.

The legacy code, access directly to the Order Table, and may even have queries in the views. It’s to clean up this part and we will use the QueryBus.

In stead of doing something like:

$orders=Order::find()->where([‘customerID’=>$customerID])->all();

we will do something like:

$orderQuery=new CustomerOrderQuery($customerID);

$orders=$queryBus->query($orderQuery);

The QueryBus knows who is responsible for returning the data, so will call to the New Order Application to retrieve the information, even if it is in another language… this is on the infrastructure part, and transparent for legacy and new application.

The QueryBus won’t return the order entity, will return a DTO containing all the info needed to show the list of orders.

At this point, we have part of the application accessing the new tables, and other parts of the applications still accessing old tables.

6. Next steps

We can continue replacing legacy code that uses the order table with new code. We will always have parts of the application that still needs to do a join between Order and other tables that we are not migrating now.

As we move the rest of our business logic to DDD, our code will get cleaned and old tables will be deprecated.

--

--

Coding CEO

I fix things. I was CEO twice, and I missed too much coding. Back to CTO again.