In the previous post, I've been writing about problems with current front-end development. And I've mentioned that there is a better way - a declarative one. Let's talk about it.
Where did the idea originate?
You've probably met Docker compose, or maybe you have some experience with Ansible or Kubernetes. All those have one in common - they define infrastructure as a code. And that's important because you are not coding step-by-step instructions on installing this or that. Instead, you just write a YAML file describing the desired state, and some operator/runner makes sure it will be like that.
The benefits? Boosted productivity through automation, consistency, minimized risk of human error and better maintainability and scalability.
I come from both DevOps and full-stack development background, so one day, about two years ago, an idea appeared: what if we take the principles of IaaC and apply them to the web app development so that we get the same benefits. Why not develop web apps as declaration files like for an infrastructure?
It sounded crazy, really! But I love challenges, so I tried that. And it worked! Since then, we've released many commercial apps developed using this way. But it's only the beginning of the story.
A visit to an application architecture
A traditional application consists of three layers: data, business, and presentation. And modern front-ends, often written as SPAs, are usually composed of an API layer, state layer and UI layer. And when we take a step further into the world of microservices, we find things like back-end for front-end and micro front-ends.
I want to stick with the last one.
If we generalize mentioned concepts, we end up with the followings:
- We have a user interface; its purpose is to display information to the users and get their input.
- To do that, we have to connect UI to the data.
- Micro-services provide those data through APIs.
- Provided data are often not in the form suitable directly for the UI, so we have to transform them.
As a result, I am more likely to describe the web front-end architecture as an integration layer, transformation layer and UI layer. And I like to call it - the integration front-end.
The role of the integration layer is to provide a connection to the data whenever it's an API call or a direct database query. The transformation layer is responsible for aggregating and transforming that data from multiple sources to a shape required by the UI. And vice-versa, it translates user input into a format required by integrations. And finally, the role of the UI layer is to map the data to an interactive and stateful user interface - it creates the bridge between data and user.
The major benefit of this architecture is a cleaner mental model. You get data on one side and map them to the UI on the other side and vice-versa.
Let's get back to the blueprints.
How do blueprints work in practice?
You can imagine a blueprint as a YAML file. It's like a Kubernetes manifest, but we call it a "blueprint". A blueprint has a type such as integration, action and view. These types correspond to the app layers mentioned above. Next, the blueprint contains some metadata and content based on the type.
In the case of integration, it contains a configuration - like an API endpoint, database credentials, etc. An action represents the transformation layer, and it's defined as a data transformation flow, described in a declarative fashion. And finally, the view is a piece of a user interface. Its blueprint contains a configuration of components, events and data sources.
So, basically, we describe the entire application as a set of configuration resources. You might be thinking - okay, and how do we define the logic?
Now reactive functional programming comes into play. We can define almost any configuration property as a functional expression. And it's reactive, which means that if the dependency value changes, it automatically updates all depending values.
This is really awesome, especially in the user interface, because we don't need to deal with update logic. We only describe relations between data and components.
We have blueprints. What's next? Next, we have a runtime. A process that loads all the blueprints and compiles them into an optimized form. Then it serves the applications as a set of API endpoints, server-side rendered HTML and client code that hydrates the app in the browser.
What are the major benefits of blueprint-driven development?
- We eliminate boilerplate code - blueprints contain only app-specific configuration. All the glue is inside the runtime.
- It's plug & play - to add a new view or a data source, we just need to create a new blueprint file. That's all, no imports, no linking.
- Clean and scalable architecture - there is no magic. Every resource has clearly defined dependencies. You can export/import entire app sections. And team collaboration is like a breeze. You can even compose your app from multiple repositories.
- It's predictable - because it's declarative. You always know how individual pieces are linked together. And we can do a static analysis.
- It's faster and more reliable - everything together gives you a huge productivity boost, and it minimizes human error. That means you can build better software much faster. And that's what I love to do.
At first, you have to get used to a different kind of thinking. You have to think in a declarative, reactive and functional way. This might be difficult for people used to OOP only, but it's worth learning it.
Because, in the end, it's more natural to think of how individual software pieces are linked together than how you are going to implement them. And it helps you to keep the architecture clean and to maintain the big picture in your head.
But what makes technology really powerful is first-class tooling.
Blueprints themselves can reduce a lot of work, but we still need to edit, debug and deploy them. And that's why we've developed Adapptio.
In the last chapter, you will learn how blueprint-driven development works in a real-world app with first-class tooling.