Imagine that you are developing an application with a lot of different modules. You have modules for manipulating companies, users, data search, transactions and payments etc. For the sake of the code organization and architecture, sooner or later (well, try to figure it out this until it’s not too late 🙂 ), you will start to organize your monolith application into modules. That should be separate folders for every module with the implementation and abstraction files for each module in it.
Do not make your modules too dependent
While developing a certain module, like a companies module for example, maybe you will come to a situation when you will need to list the user emails or anything else connected to the users on the company dashboard or all companies table. It is a very common situation for developers (that includes me too) to create tight relations between the companies and users modules in these kind of situations. The reasons are usually reasonable, we are doing that so we can reuse the code that we’ve already wrote in the other module so we can avoid code repetition and reuse the same piece of implementation, but we often face another problem later. As the application code base becomes larger and larger, we realize that changing some class or method that we are using on multiple places in the same module and in a couple of other modules is a real pain. That kind of code refactoring usually cost us a lot of time, regressions and headaches.
Instead of doing those critical relations between modules, you can do that by adding a separate layer of abstraction that will do that. Call it as you like, an interactor layer, proxy, gateway or maybe you will find a better naming, what is important is that this layer should be responsible for interactions between modules. Now, if you need to change anything related to the business logic, you will not make changes to the module abstraction, you will need to deal with the interactor layer. This kind of architecture setup will lead you to better and easier way of separating a concrete module as a microservice for an example.
In addition to this, you can read more about Domain Driven Design.
Use interfaces in dependency injections
When calling a builder class from a particular module, use interfaces. Yes, interfaces sometimes might complicate your coding, but they are life savers when it comes to refactoring code implementations.
In some of my previous blog posts, I was writing about re-indexing Elasticsearch data. Imagine that you need to build a module for indexing data on Elasticsearch. After I built my module for indexing, after a certain period of time, I realized that I will need to do a major refactor of the module in order to optimize performance, the number of queries executed etc. But, I also needed the old implementation to be present in the same code base until the QA team test all possible scenarios and write all tests needed. If something went wrong, I needed to quickly return the old tested and stable implementation. It was a major and heavy refactoring and we couldn’t be sure that everything works smoothly, but the deployments were constant and the module was a part of a large monolith application.
Using interfaces in dependency injections here is a real life saver. You can write your new implementation in a separate folder with separate namespaces and the only thing you should do is to implement the same interface as the old code implementation and to change classes and interfaces bindings in your provider, if you are using Laravel for example. But be aware, that this is just a concept and it will work if you are using Spring or Django or any native PHP, Java, Python or whatever code.
Using interfaces is much more smooth when it comes to whatever class or method refactoring. You are just writing your new implementation, you do not even need to keep the old implementation. The important benefit is that you wont need to change every single class and method where you are using this module or piece of code. The only thing you’ll do is just implementing the same interface. This might be an over engineering moment when it comes to smaller applications, but it is a common problem in large enterprise apps.
Use type hinting in crucial parts
If you are using a static programming language like Java, well, you will need to use type hinting. But when it comes to dynamic programming languages like PHP, type hinting in PHP is not mandatory. But that doesn’t mean that you shouldn’t use it. I know that this is a real debate, but when it comes to crucial parts of my code, classes and methods that I am using in multiple parts in my code, I like to use type hinting.
Type hints will make your crucial parts more usable by other developers, since they will be aware of your main rules related to your method: of which types should be the arguments and what should they expect as a response. If you have only one usage of a concrete method, you’ll probably be safe if you avoid type hinting, but if you are using that method all over the place, type hints are your friends, believe me 🙂 .
Consider migrating to microservices
Yeah, I know, it is the big buzzword again. All of the above things will probably work without migrating to microservices or anything similar to that, until the moment when you have to use the same module in some other application. Lets assume that the “Indexing data to Elasticsearch” module was only used by your main monolith application. But if you have an administration application which is used by your clients in order to reject or approve users, content, generate reports or vouchers and sending emails? And you are safe until the moment when that administration app will need to use your Elasticsearch module. Then you will need to re-write the same code base from your module to the other application code base. The most frustrating part is the maintenance. You will need to make every change on two places every time you will need to change something. Also, consider the fact that deployments on such a large and complicate apps become heavier and heavier.
You can make this module as a separate package and update through composer at all apps every time when you have commited new changes, but this solution is issued in a case when your actions are actually queued jobs.
In such a case, the best solution is to migrate the module to a separate microservice and you will need to use a language agnostic message broker in order to communicate with the service in a queued way. There are plenty of a powerful solutions for a message broker out there:
Or you can use a simple Redis, but I suggest you to research more about its persistence issues.
Also, a big benefit of migrating such crucial parts of your application as a microservices is the fact that, after migrating, you are making your module language agnostic and independent. This means that if your interactor layer, or your main part of your application is in PHP, and after a certain period of time, you realize that PHP is not the most suitable technology for some of your modules or services, you and your team can re-write it in any programming language you think is more suitable for that job, using any technologies and tools you like.