“Big Ball of Mud” is the current state of the UI architecture, despite component-based composition.
Components are not enough
Components and composition are just one piece of architecture. You still need to know how to compose pieces without recreating the “Big Ball of Mud” using a new (not really) shiny components-based approach.
Components are too generic to actually solve architectural problems. They are comparable to the bricks. You can for sure use bricks to build a house, but there is so much more to building a house that bricks aren’t even 10% of what you need to use.
We spent decades defining methodologies and principles for great architecture and yet here we are — every new job will surprise you with new ways of creating an utterly broken architecture.
Is everyone stupid? Did nobody read about SoC, SOLID, DRY, KISS, WTF, GoF, GRASP YAGNI, WTF, Atomic Design, DDD, Onion, Clean Architecture, etc?
I am sure everyone heard at least one or two of those and maybe even understood them, so what’s the problem? Why can’t we have a better architecture in practice?
Maybe all we need is another, “better” architecture that solves all the problems? (Not really)
Theory vs. practice
These are the most popular reasons why your organization will inevitably fail at the architectural level:
- Many well-known principles and methodologies are conflicting.
- Requirements change over time.
- Organizational restructuring.
- There are no generic good ways to enforce a particular architecture.
- Poor communication of the applied architecture.
- Practical implementation difficulties.
Let’s take one example: DRY — Don’t Repeat Yourself vs. High Cohesion — keep related code close.
In theory, they both sound great, though both of them fall apart when you have 2 independent features that need the same function. I understand this is too theoretical and it never happened to YOU in the real world /s, but bear with me for a second.
As soon as you have to share a function between 2 features you have 3 options:
- Let feature A import that function from feature B directly and marry both features together in the sense one has to know about the existence of the other.
- Move that shared function into some “shared” directory and use it from both places.
- Copy that function into both features
If you are reusing that function, you are breaking “High Cohesion”. If you are copying — you are breaking DRY.
And this is just one simple example, there are many more.
In the modern world of startups, every company starts with some small prototype that can be manufactured quickly and then hopes to evolve it into something better.
When building things quick and dirty — you don’t have time for good architecture, let alone you don’t know yet what “good” means for that particular product.
Product is changing and so are the requirements. You either have decades of experience and can predict what architecture needs to be because you saw this very same thing happening already or you will start with some baseline architecture that minimizes your risks.
It’s not a secret that the architecture needs to not only be designed around the product requirements but also around the people working on it.
If you have a team of 2 engineers, exchanging information, agreeing on architecture, and keeping things consistent is one million times easier than having a team of 20 people. (please teach me the math here /s)
Every product starts with a small team and then adds the team members over time. People join, people leave and communication pathways change all the time.
Can we have an architecture that is correct no matter how many people work on the product?
Enforcing an architecture
If a magical tool existed that could enforce every principle and every architectural decision that is used on a product, we could have a consistent architecture and much better communication of it.
People don’t read docs. They don’t read not because they don’t know how to, but because they don’t know what those words mean in their specific context, and learning it is hard work.
Tools can tell a person to read something when they are about to make a mistake or create an inconsistency, but creating such tools is hard because many violations of principles are hard to detect in code.
We definitely need tools to communicate architecture, and yet, do we know many tools that try to do that at least on some basic level?
Documentation is hard. Documentation of principles and methodologies is harder. Documenting them well as a by-product of some product work — impossible.
Your internal documentation around principles and architecture will always suck unless you have people who have a serious level of dedication for the topic — aka “architect”.
How many companies have you worked for that have people with architectural roles?
Tactics vs. logistics
One dude said once:
“The amateurs discuss tactics: the professionals discuss logistics.”
Tactics in our context are the same as abstract principles and methodologies. Those are just words that don’t tell you what to do in your context because they are too abstract and conflicting.
The missing piece in the industry is a set of principles that can be well understood and applied in practice that go along with tools that help to enforce them.
This is the logistics analogy.
Am I here to just criticize the current state or to move the needle? Well, without agreeing on critiques we can’t move on to something better. Can we come up with something better? I believe so.
I expressed some ideas in my talk in 2018 with the title “Feature Driven Architecture” https://www.youtube.com/watch?v=BWAeYuWFHhs
There is nothing fundamentally new in it, it is a collection of well-known ideas put together into a single practical architecture.
If you like and retweet this article hard enough, I might work on defining those ideas and examples more clearly in this repository https://github.com/kof/feature-driven-architecture
Features are everything
The word "features" is the most common to describe application-level components. An application will have features added, removed, modified. It will have big features, small features, life-changing features. Features that make you proud of yourself, features that will embarrass the entire company. Features are the most important piece of every application.
This is why it is so important to make it your life goal as an architect of an application to make sure a feature can be added, removed, or extended with the least amount of work.
Being able to completely remove a feature without leaving pieces of logic left behind spread over the entire application is the key to a maintainable product.
An ultimate test for any architecture should be to remove a feature directory from the file system and the application should stay fully functional without it.
An ideal feature contains everything it needs.
Principles of a feature
These are the most important principles that have to be followed in a Feature Driven Architecture:
1. A feature can not know other features or its consumers.
It’s simple — you will break one feature when you change the other otherwise. You also can’t remove one feature without affecting another, if you break this principle.
2. A feature has to express its dependencies declaratively.
If there is data or any other dependencies — it has to express them declaratively and a consumer should be able to provide them.
Much like a component, features don’t live in a vacuum, they have dependencies. To name a few: data, static artifacts like images, libraries, functions shared between features, knowledge of app architecture, build system, and more.
Dependencies are what makes it hard to build features that are fully encapsulated and integrate well with the rest of the application. That’s why on the architectural level we can only define basic rules of behavior, but not the specifics around implementation details because those will vary depending on your stack and particular business requirements.
An ideal feature doesn’t have dependencies.
3. Be disposable
Much like a component, a feature has to be disposable. You need to architect the feature so that you can remove it easily. That’s the whole point.
If one feature doesn’t know any other feature, there has to be something else that knows all the features, otherwise, we will render a vacuum.
Screens have to follow the same principles we described for the feature for the exact same reasons. In addition, it acts as a features manager:
1. Doesn’t know about other screens
2. Renders the features
A screen can import a feature and access its public API, including its rendering interface and its declaration of dependencies. The screen’s main goal is to render the features on the screen.
3. Provides dependencies
In case a feature can not satisfy its dependencies itself, a screen will have to provide them. It could be static artifacts, data, or anything else. Important is that if a feature can provide those things itself — it should. I can only see the need to express dependencies when the dependency is outside of features scope or it is something 2+ features on the screen depend on, so it would potentially make sense to hoist the logic to the screen.
4. Connects the features
Sometimes one feature depends on something that some other feature has or does, but since features can’t know about each other, this dependency has to be expressed via a simplified protocol.
If the dependency is data — one feature has to express what data it requires via an interface and the screen will pipe that data from one feature into another while trying to keep the least amount of knowledge about its contents.
The more screen knows about each feature — the harder will it be to remove a feature from the screen.
Sometimes a set of independent features is used on 2+ screens together as a set. You don’t need a cluster to achieve that if a feature is fully self-contained, but if a feature has dependencies provided by the screen — you would have to duplicate the setup logic that wires up those features 2+ times for each screen.
If that setup code a feature needs is trivial — you may want to avoid having a cluster to avoid the need for this additional abstraction.
But if you end up needing a cluster — the rules it follows are similar to a feature:
- Doesn’t know about other clusters
- Doesn’t know about screens
- Knows all features from that cluster and exposes a declarative interface for the screen.
- Provides features with dependencies just like a screen.
Sometimes you need to share a function between features, screens, or clusters that has business awareness. This is where a “shared” space comes in handy.
Shared space has one rule to follow — it can’t directly depend on anything outside of “shared” except the libraries. It can’t depend on a feature, screen or a cluster.
The final and the most low-level piece is a library. A library has no knowledge about business logic. It can be published to or installed from an Open Source repository and can be shared between businesses. You can have a lib directory inside the product or you can treat the installed package from an external source as a lib. One example is the node_modules package, if it doesn’t contain a business-aware logic — it's a library.
To sum it up, my proposal is to split an application by a number of principled types that would let your application be maintainable, guided by the need to have built-in discoverability, disposability, and scalability. The proposed types are in the order from higher to a lower level of abstraction:
I am certain the constraints I expressed are basic and there is more to it. We need to work on a well-defined spec for them. I am also certain that enforcing those constraints should be automated one way or another and we need to build tools and interfaces to make a consistent architecture.
Top comments (1)
These last months I have been thinking about something like this, I would like to create a product that helps in some way to solve these problems, and I think that right now there are many tools of high/low level abstraction, but there isn't much in between these two, and that may be one of the problems that doesn't help us to have a consistent architecture.
btw greate article!