Component Based Architecture
Revamping the architecture thoughts
Almost every application, framework, online tutorial, everything these days is around the idea of layered architecture, where we have controllers, service, and data model layers.
We tend to apply a design pattern or code style blindly without understanding the reasons behind that design principle or its trade-offs — Cargo cult programming.
We think we should do it because that's how everyone else does this. It is hype and fashion. It is a best practice. It has many upvotes on StackOverflow.
As a result, we mimic instead of thinking.
We make bad, uninformed decisions instead of adding value.
We lean towards what is being hyped. After all, Machine Learning won’t make from us better software engineers.
Finding someone who can talk software engineering these days is like trying to find a needle in the middle of the ocean.
Software architecture design is thought to be a conceptual thing, a bunch of boxes and lines, but it is the structure, and journey getting to that structure and communicating it.
Teams struggle to communicate the design of the software. It is complex, too detailed, scary, etc. We don’t know how to express ideas, level of detail, and notations. We lack the understanding of the underlying principles of design.
It is seldom to find someone who is familiar with UML. After all, they are not relevant to the market compared to someone who is doing Kubernetess.
And while the aim of design documents is to serve an input for the implementation and ease the communication about the software code, they end up not reflecting the code.
Did we implement the code as we designed it? No. The code doesn’t mirror the architecture, these abstractions. In fact, nobody looks at the architecture diagrams anyway. Why?
- They are out of date.
- They can’t communicate or don’t communicate.
- They don’t match the code.
- They are just a bunch of fluffy boxes.
4 of the most common design architectures will be discussed below highlighting the component-based architecture.
Perhaps, this is the most widely used, it is everywhere. Almost all the applications are built around it. It is easy to understand and good to start.
In this pattern, we organize into horizontal slices. The number of layers can be different from one application to another. Generally, we have:
- The web layer: handles incoming requests, renders HTML, etc).
- The business layer: enforce the business rules on the data (customer can’t withdraw from an empty account)
- The data layer: connects to the database to query, and update the data. These are often called data access objects (DAO).
When the request comes in, it goes from the top till the bottom.
But Why? separation of concerns, decoupling, or maybe because we have been told that this is a good thing.
Well, it might be. But the question here is, does the architecture design diagrams reflect the code?. We think of components but we write layered architecture. These components don’t exist in the code, nor the programming language has a layer or component keyword.
And does each layer only calls the underneath layer or can skip around? Does each layer stay nice and clean?. Controller sometimes calls the model, and model calls back the services, calls are all over the place since they are public.
The constraints we say in documentation or by telling developers can’t be enforced. We might be able to go through code reviews. But, we are all humans, and to deliver faster, we might be forced or tempted to accept the bad code anyway.
Ports and adapters architecture
Onion architecture, and hexagonal architecture are all around the idea of decoupling the domain objects from its implementation.
While in layered architecture each layer is coupled to the layers below it, which means a small change will lead to change in all the layers, that is not exactly the case here.
Yes, we do have layers, but the inner layers are the abstractions, while the other layers are the actual implementation.
For example, inside the order domain package, we have
Orders as an interface that defines the fields (id, date, customer) and behaviour (create, update, etc). They are all abstracted, no implementation.
In addition, we also have
OrderService which defines the implementation of use cases and the business rules around them. These rules can be
placeOrder(), cancelOrder(), etc
Everything inside the order domain package is independent on the outside world. So, we can have different classes, say
MySQLOrdersRepository, JDBCOrdersRepository, etc that implement
Order . Does the
Order need to know who about these classes? No.
For the controller to call the service layer, it passes one of the Order implementations by which the service layer will use to access the database and enforce the business rules.
private OrderService orderService
= new OrderService(new JDBCOrdersRepository())
This decoupling gives the benefit of adding or removing the implementation (adapters) without having to change the underlying abstractions (ports).
Still, it might be hard to have 1–1 correspondence with the design diagrams if they are all about components.
In this pattern, we organize the code based on what it does from the functional perspective. It is a vertical slicing. We group together code related to a certain feature.
Only the controller is exposed to the outside world. Everything else is kept package protected: Can’t be called from outside the feature package.
It has higher cohesion as all code is one place. And if a feature is considered to be a component, then we can easily match between the code and the design diagrams as the code now reflects the architecture design.
But what if the service class needs to call another service class?.
A hybrid approach between layered and feature-based architecture.
Instead of having a layered approach, horizontal slices, we instead split the application vertically into modular components, just like as we did with feature-based architecture.
A component in this context is a group of related functionality that resides behind a nice and clean interface.
A couple of notes to notice here:
- The controller is part of the component. Though we can split controllers (routers) from component related code to separate the handling of HTTP requests from the component itself. By the way, not every component needs a controller as an Email service.
- Components can talk to each other through their interface unlike in feature-based architecture where service classes are hidden, and the only way to communicate is through the controller.
Now, we aligned the architecture view of the world and the code view. It makes the code easy to understand and discuss because we can reflect back to the code.
The decoupling we got is by not allowing anyone to nudge the component code from the outside world except through its interface.
Each component is encapsulated in its own package. It has all the implementation details like accessing the database, unlike in the Ports and Adapters architecture where implementation is split from the abstraction.
In a component-based architecture, we can have layered architecture wrapped inside the component. For example, a payment service (component) can have three classes: controller, service, and model layers. In this case, the layered architecture is not about the structure it is rather an implementation detail.
This flexibility between component is really important since they don’t need to know about the internal workings of each other. Components just communicate through their interfaces.
That’s the core design concept behind Microservices. Later, if needed, it becomes easier to migrate to microservices architecture.
After all, these design principles and patterns are all about design thinking and modularity. A good architecture allows agility: To extend and change as a response to requirement change.
If you suffer from a monolithic application, it is perhaps because you failed to design a modular monolithic. And so, the microservices will end up being messy too. The bottom line is, if the component architecture is hard, we can’t do microservices, because both are based on the same principle.
Choose microservices if they give you benefit not because the monolthic is messy. In most of the startups, the number one priority is to move fast. Spending money and effort to do microservices will hammer the progress significantly.
Who is using it?
I don’t know.
Anyone else? …
The C4 model is a formalized approach used to visualise software architecture based on the idea of components.
A software system is made up of one or more containers (web applications, mobile apps, desktop applications, databases, file systems, etc), each of which contains one or more components, which in turn are implemented by one or more code elements (e.g. classes, interfaces, objects, functions, etc) — source.
There is no enforced notation, but having a common and consistent set of notations pays a lot in understanding and communicating these diagrams.
And since this model split the architecture into levels of granularity detail as we go from Context (level 1) to Code (level 4), it resembles having a map. We might wanna zoom in and zoom out to view the whole picture or dig deeper.
Furthermore, it makes it easy to take it and create other models like deployment diagrams; how containers mapped into the infrastructure. That is useful as we need to have different views since it is hard to show the entire design in one picture.
How do you know which pattern is better?
We do need to think. What do we need? What works best? We shouldn’t blindly apply the same concept over and over again just because all the folks are doing it. Decomposition is never a new topic, and there are different ways to decomposition. So, it requires an up-front effort and thinking.
One key attribute though is agility. Having a good architecture must be easy-ish to scale and tweak.
Another attribute is publicity. If everything in the above patterns is “public”, there won’t be any difference except just the folder structure.