In the world of backend software development, the architecture you choose can greatly impact the flexibility, scalability, and usability of your applications. One such practical and efficient architecture is the modular layered architecture.
The modular layered architecture breaks down an application into separate modules - each with specific functions. The higher level of organization facilitates a cleaner, more maintainable codebase. In this architecture, the Application Domain or API layer generally holds the reins - it knows the entirety or at least the crux of what the application is supposed to do.
Consider an example, a command like dealing with some hardware
api.SwitchOnLight(green);
or dealing with a vector database
api.UpsertDataSourceAsync(string context, string url);
.
Here, the API knows about the functions it's supposed to perform, that is, turn on the light to the green color or update an existing vector in the vector database.
When the API is requested to perform these operations, it communicates with underlying modules to carry out the task. For anything related to database operations, there comes the Data Access Layer (DAL). This concept is consistent with the Repository Pattern as interpreted by developers.
However, a question arises: what about when the API has to deal with external systems like a lighting or a hardware system? Here, we need an architecture that seamlessly integrates all kinds of external services - a more specialized layer such as a Hardware Access Layer (HAL) for hardware interaction along with DAL for database interaction. So, we rather talk about Service Layers than Repository Pattern, which is a special case in more simple applications.
Peek into the realms of Windows, and you may encounter HAL - an age-old concept that continues to deliver. From the perspective of the API, to switch on the light, the code might look like this:
SwitchOnLight(color)
{
_hal.SendMessage(new Message{ clr = color, intensity = default});
}
The HAL implementations include HttpAccessLayer, ZigbeeAccessLayer, etc., each designed to communicate effectively with a particular set of hardware. The only thing they need to know is how to speak the hardware's language, not anything specific about the application.
However, note that a design flaw often seen is letting the DAL or HAL know too much about our application. Continuing with our earlier example, the following design would not be ideal:
UpsertDataSourceAsync(string context, string url)
{
_dal.UpsertDataSourceAsync(context, url);
}
In this case, the DAL knows about url and contexts. These are application artefacts. It's essential that layers are as ignorant about the application as possible, meaning completely ignorant. The main idea is to make these layers as reusable as possible. Independent on reusability, it is good to follow single responsibility pattern in each component. Consider transporting this layer to another application that needs to work with cars and houses data - having to handle context, url or any application specific detail might not be the best approach.
A better approach for DAL design would look like this:
UpsertDataSourceAsync(string context, string url)
{
_dal.UpsertVectorAsync(dataSourceCollectionName,
new Payload{ url = url});
}
Here, the DAL takes the responsibility of creating the payload in the given collection of the vector database. The API that implements UpsertDataSourceAsync is the only player here that needs to understand the bigger picture (context and url), allowing the DAL and HAL to remain efficient, simple, and reusable.
To conclude, the modular layered architecture truly shines when it comes to separating concretes from the abstracts, enabling the creation of a versatile, reusable, and maintainable backend architecture.