Eliran Natan, the R&D software architect at Axonius, has been building scalable systems for the last 10+ years. He was the keynote speaker at Lemon.io’s meetup Better Front-End for Better Results, organized in May 2022. Here’s an article on his speech.
Reading about system architecture and front-end solutions, we often encounter the fundamental discrepancy between monumental patterns for back-end systems (domain-driven design, clean architecture, CQRS, microservices) and a simplistic software layer composed of components and solutions.
The core of the problem might have something to do with the assumption that presentation logic is much simpler than business (backend, smart patterns) logic, and it doesn’t require any sophisticated level of architecture.
Perhaps that was true ten years ago. Today’s reality is different: UI’s aim at delivering higher added value is increasingly getting more complex and partly similar to games: the more added value we want our UI to deliver, the more complexity we have to tackle in FE.
Just one example. Let’s presume your system supports some multi-level query language allowing you to build up a query and retrieve complex data.
That’s a typical situation not only in cybersecurity but also, say, in healthcare products.
Two options for solving the front-end issue
First: allowing users to type in the query, send it to BE, and get the data back. That’s the variant with zero added value to FE and zero complexity.
Second: allowing users to build their query with some graphical intuitive drag-and-drop interface letting to move entities and make visual connections. However, added value brings a lot of complexity — new entities, relationships, and ideas aren’t part of your business logic, but you still need to tackle them.
I have a specific name for ideas and entities emerging once we try to deliver some added value using front-end possibilities. I call them the presentation domain. That’s what I’m talking about when I mention data-driven front-end.
The main UI frameworks allowed us to build very fast and responsive UIs. Unfortunately, these UIs are corroborated by numerous problems. The root of these problems is a way of thinking about UI.
These frameworks are making us think in terms of components, not presentation logic.
Four facets of the big problem
We could segment this big problem into four main issues.
It is tough to test FE components on such a scale: they become very complex and engaged in state management, and asynchronous side effects complicate the testing processes.
Growing organizations want to maintain multiple platforms, move between frameworks and support different types of technologies with the same front-end.
However, many FEs are heavily dependent on their frameworks and technologies — so we often see the organizations have to rewrite entire apps and handle efforts of maintaining those different FEs.
A deeper, albeit just as frequent problem is the ability to split your codebase and divide it between a few teams. We often see this arbitrary division of responsibilities between teams — sometimes, it’s about owning components and pages or views.
The feature or user story we want to implement most often falls into the responsibility area of multiple teams. They must coordinate efforts and maintain efficient communication, which can become a severe bottleneck.
FE code is often composed of made-up concepts and synthetic structures that don’t mean anything to domain experts or product owners. It’s hard to follow up on this terminology, usually invented by devs and documented by them for inside usage. On the other side, user feature requests are often expressed in the domain language that should also be translated into technical lingo.
Why do we keep bumping into the problems while scaling our front end? In my opinion, the root cause is mixing up WHAT and HOW. What do I mean? Read on!
Front-end solutions mostly mix presentation logic (concepts) and technical implementation details (persistence layer, infrastructure, framework, etc.). What we want is something completely different.
What should this “different” look like?
Let me familiarize you with the concept of the layer structure.
The layered structure of the front-end
Each system consists of several layers. Front-end is no exclusion.
The core layer (pure logic) captures and describes entities and processes in our presentation domain (should be purely functional, irrelevant to any implementation details).
The adapter layer mitigates collisions of the core layer and the outer world.
The virtual infrastructure consists of solutions we want to use for delivering core aspects to the external world — databases, HTTP calls, cookies, UI, and frameworks.
The layered architecture is heavily inspired by the clean architecture and is different from what we’re building today. The clean architecture highlights the importance of dependencies that point inwards.
The core should be independent of the adapter layer, and the adapter layer should be separate from the infrastructure layer.
The MVVM pattern
Our architecture vision is presented in the MVVM pattern, which is a straightforward way to look at data mining. Its components are View, View Model, and Model (hence MVVM).
The View is the actual presentation, components the user can interact with, and part of the infrastructure. View communicates with the View Model that arranges all the functionality needed for View. Each View has its View Model.
View Model will communicate (send requests and pull data) with the Model (isolated part of the core). ViewModel is a React hook, a component, and a function — effortless to test as well.
We’re not just separating UI from our presentation logic which is concentrated on the Model — we also separate persistence solutions or other infrastructure concerns (calling the server, keeping stuff in your local storage).
One more concept matching our layered architecture vision is the repository interface — a wholly abstract and logical part of the core.
The repository interface can be simply implemented in Typescript.
Once you want to add some data source, you have to write a matching adapter. You implement the repository, you connect your data source, and the repository will have to implement the interface defined at the core level. It ensures that the core level is entirely isolated.
The dependency injection
Another important issue I would like to tackle is dependency injection.
We don’t want our core to import actual data sources (repositories) because that will make the core dependent on external stuff and conflict with our vision of inward-pointed dependencies.
We can solve the problem by injecting the actual data source, the actual repository, into our core.
The product’s provider (React Context) can implement the actual adapter (repository) or import it — and then any component from the product list that needs this solution can be wrapped in this context.
It keeps the model extremely abstract and independent.
The combination of the MVVM pattern and the Repository pattern helps us form a technology-agnostic front-end.
Within this concept of FE, the core layer doesn’t care which UI or framework you are using and which data sources you are using. The core layer is just a very abstract description of the entities and processes within your presentation logic. It’s much easier to test and completely reusable: you can use the core app to plug in and out different persistent solutions, frameworks, pr UIs.
This technology-agnostic front-end solves two of our problems — testability and reusability.
Until now, we discussed wrapping layers but didn’t mention the actual presentation of the model itself. It’s crucial for addressing splittability and expressibility problems I will discuss next.
For starters, I want to mention one particular pattern from DDD called strategic DDD. It’s about separating parts of your domain according to business aspects. Let me explain what I’m talking about.
Suppose you have a classic e-commerce platform.
Most such platforms will allow users to search for products, browse, select, and make an order. After the purchase, there can be possibilities to track and manage deliveries and get support in case something goes wrong.
Splittability and subdomains
It makes sense to target and split these functions into subdomains. I offer the following division:
After the split, domain experts can formulate the specific language within each subdomain, capturing the universally acceptable terminology to describe the inside processes and entities.
This is what we call bounded context. It helps to speak about the methods very clearly, using unambiguous language.
What do we gain from this split?
The simplicity of implementation
Each term is defined from the perspective of the actual context. How’s that? Let’s analyze the definitions of a user in various contexts.
- Product: user means nothing;
- Order: the user is the one who has the cart with products and chooses a payment method;
- Shipping: user has a list of shipments;
- Support: user has some issues or tickets.
Tackling the splittability problem
All user requests in this paradigm fall into some specific, isolated context. Owning a particular context, a team could deliver the UI feature with little to no communication with other groups.
This point seems to be a part of a broader theory related to Conway’s law. According to it, organizations duplicate their structure into the software they build.
Business-based splitting is probably the most efficient way to solve the autonomy issue.
If our software reflects a business perspective, we must arrange teams accordingly. We can take it further and allow each team to build its app. Indeed, it will make teams even more autonomous — they will be able to choose their stack, system design, or working standards.
Moreover, we could allow autonomy even on the API level, splitting it into smaller APIs, each of which supports a specific micro frontend.
One potential problem of this approach is UI consistency, which can be solved thanks to solid design systems.
I’ll give an elementary example of using tactical DDD to model our core app.
Imagine a simple online shopping cart with added value: if you drop one more apple, you get it for free.
How can we apply tactical DDD to make it domain-driven?
Tactical DDD helps us turn concepts into entities, value objects, and formulate the relations between them.
Which entities should be consistent with each other? We often see complex presentation logic composed of many moving parts that are hard to synchronize.
Typescript allows us to pit rules inside definite objects. When implementing UseCase functions (the ones our model exposes to the outside), we must think about everything we have gained here.
We’re not introducing a new programming language. We’re introducing a new architecture where it’s effortless to understand what’s happening; you don’t need to understand the whole system to contribute to it. Without this kind of architecture, you get monolithic FE, complex presentation logic heavily tangled with implementation, documentation, discussions, and coordination of teams.
What are the main benefits of the strategic DDD?
- Simplifies the implementation;
- Aligns splits with actual domains;
- Makes your team autonomous;
- Helps make the core app much more readable and understandable for domain experts;
- Expresses core ideas and natural concepts of your domain.
The scale and type of the most fitting solution for your FE depend on its complexity. You don’t have to implement everything. Besides, most simple apps won’t even need to be framework-agnostic. If you have an exact domain and simple apps to handle it, think twice before investing in DDD (and when you finally decide to, consider your available domain experts pool). Think of the complexity your UI needs. Effects of DDD will become visible and tangible in the long run — so if your enterprise is short-scale, perhaps DDD is not your cup of tea.