This post is the continuation of The Tug of War, where I explained on a high level what pushed us to the route of a big code refactor. In this post I’ll explain the details of how we refactored our codebase and changed our architectural style in order to cope with the company’s growth.

Without further ado, let’s see the beast, the Weengs genesis code. This code sample is pre-spaghetti from 2012. The style is PHP 4 without any framework, the routes were hardcoded Nginx directives, with a bunch of God Objects. It was the real deal, take a look!

But before you judge too much keep in mind this code landed us a big warehouse in London Kings Cross, uh? This thing had so much energy it was at the tip of spontaneously combusting. This was the seed of an entire business.

Then this code was translated into PHP 7 spaghetti inside Laravel apps, everything in the controller, procedural. Using Laravel was a great decision, it’s a well designed framework that accommodates the best modern development practices, kudos to Laravel! A framework should serve you, not the contrary, and Laravel does that brilliantly. Let’s see how it was!

It is from this point that the challenge began, to give an idea, logic like that in controllers was duplicated across different apps. To tackle this problem, reduce duplication and make our code more readable we created the following rules:

A Core package would be created to receive DB models and migrations, representing the high coupling on data level because of the shared database.

Eloquent models would remain anaemic without any logic in them, they’d be only used as they should: a handy tool to help you with the database.

We couldn’t rewrite the code, we didn’t have the resources. The procedural spaghetti would be pushed to application service classes, each class representing a business action. Through these services our apps would interact with the domain.

The services would be ignorant of infrastructure, relying instead on the good abstractions provided by Laravel.

Each application service class would belong to a context: Operations, Marketing, CS, Fleet, etc…

Any shared logic would go to base abstract classes in the Core package.

Services could throw three types of exceptions: ValidationException, BusinessException and a more specialised exception (if needed) extending BusinessException.

Minimum Testing standard was created, where we’d cover the “happy path” of the business action (where everything worked and the action was successful) plus any exception the service might throw.

Each service would be stateless with strong contract, receiving a request object and returning a response object. Request objects would be value objects that accepted only scalar values and perform validation. Response objects would be responsible for transformation, acting as some sort of ACL (anti-corruption layer) for the database, they’d receive data and convert the data to an array.

Something like this:

Let’s see how all of this translated into code, this is a simple service example:

This is the service request object, where validation is performed:

And the service response object where transformation is done:

And finally that’s how services are used in controllers:

For simple read operations we found application services to be an overkill and created “data fetchers”, which are read-only services that don’t need neither tests or request objects.

All this effort was to improve readability, increase testing coverage and stop using Eloquent directly in the apps so in the future we could evolve our data schema. But as anything in life this solution has its pros and cons:

Pros:

By receiving and returning scalar values to and from these services we can guarantee a sealed domain, it guarantees no logic leaks into the domain from the application and vice-versa.

Just by looking into a service class we are able to know what’s going on. The namespace gives you the context of the service and what it does. The data and validation on the request class tells you what the service needs to work. The logic in the service and its exceptions shows how things are done. And any data the frontend needs can be seen in the response classes.

Minimum testing these application services is a joy, very easy and intuitive. Just assert the happy path and cover any exception the service might throw.

Cons:

The domain is a bit weird, it is composed of a hundreds of application service classes with procedural code inside. Far from a more refined DDD domain and its tactical patterns.

The deploy continues to be monolithic, any change on the DB will cause a rebuild of all apps and APIs.

The codebase is highly coupled to Laravel as we heavily rely on the framework’s abstractions.

Conclusion

Keep in mind we had to be very pragmatic with this solution, we weighed what we had to do against what could be done. Despite not having an ideal solution we were able to create a well-architected application complying to most of 12 factor directives. We call this part of our architecture the Main Domain and we fight tooth and nail to prevent it from growing. Any opportunity we have we decompose the Main Domain by extracting independent services from it. And of course adding new features to the Main Domain is a no-no, new stuff always come as indie services. But now we’re entering the realm of architectural concerns… Which will be covered by my next post. Until then.