The only constant is a change or how The Explicit Architecture can save the day (Part I)

Milan Pevec
9 min readJan 1, 2022

--

The Explicit Architecture by Herberto Graca

Writing concurrent Elixir allows applications to be scalable. But we want them to be scalable also in the context of growing business requirements. Business wants changes to be applied fast, to measure the feedback and learn. At the same time not to break existing functionalities. Last but not least, developers want high clarity of the code base for easier/cheaper maintainability and for less stress in life.

So how should we set up the (Elixir) application architecture? Which one should we choose, since we have so many of them, like:

just to mention a few? The answer you will hear is “depends”. I hate this answer. It is like the answer “yes and no”.

In this two-part series of articles, I would like to show you what works for me. I would like to show you, how I adopt the Explicit Architecture in Elixir, created by Herberto Graca, which is actually a combination of all the above-mentioned architectures.

And the raison d’etre of this article is to present it by a real example written in Elixir. Our guide will be avoiding theory as much as possible because we have enough of it online and offline. The source code can be found on my Github account here.

But before I continue, I need to clear some things out. There are many versions and adaptations of the architectures. Choosing one does not mean, that that is “the way” and “the only way”.

The chosen one is not a silver bullet of the software engineering.

No matter what you hear from some thought leader or authority. Also, it's always a matter of taste.

Now that this is being cleared, let's jump in!

The onion is the foundation of every good soup

For better understanding and to not get lost immediately, we will for starters ignore the main detailed diagram of our architecture as drawn by Herberto (shown at the top of the article).

Rather we will draw a more conceptual diagram, a mini-map, that we will keep in our minds through the rest of the article. It's based on the Onion Architecture and looks like this:

Simplification of the Onion architecture used as mini-map through this article.

There are two important things in the diagram:

  • The direction of dependencies is towards the centre. That means “outside” know about “inside” and not the opposite. For example, the domain in the centre does not call infrastructure (like DB). “Inside” just defines the contract (interface or behaviour or port) and “outside” implements it (adapter) — which is actually the Ports and Adapter (Hexagonal) Architecture feature. The contract is then a part of the inside because only the inside knows what it needs! Please check the video Hexagonal architecture to find out more.

In Elixir by using behaviours and practising “program to behaviour” we can by configuration define which adapter (implementation of behaviour) we are going to use. As we gonna see later. So:

“Program to behaviour” is the concept to remember and plays a key role.

  • Onion architecture does not use the classical layering of the app ie. one layer on top of another (so-called lasagna). Instead, we are using contexts, where the context represents a business workflow. And each uses a piece of the specific onion layer. In other words, we are not putting all controllers in the same directory etc.

Contexts are bounded ie. isolated between each other. They should not call each other directly (or as little as possible in case we are not using events for the in-between context communication). What belongs to a specific context, as we will see later, is not an easy job to do. Decisions here can affect the whole system, so we need to be careful.

Before jumping in, I want to do a sneak peek of my Elixir directory/file structure — just to make a little break from theory:

My example folder structure of the Explicit Architecture. Github source.

As you can see in the picture, I’m trying to maintain the structure as defined by the onion. The top two directories represent contexts, inside we have application core, infrastructure directory, rest controller and some behaviours. No more details – I’m keeping information extremely compressed for now to avoid overthinking.

Application core

It's the place where the business lives. But what does it mean “lives”, what business needs “to live”? With simple non-technical words, we can identify four groups. Don't forget, we are inside of the onion and we want to peel it deeper.

The four groups are:

  1. Models with business rules.
  2. Repeatable business logic.
  3. Use cases (that works with the above two groups by orchestrating them).
  4. Agreements about business communication with the outside world.

From now on, everything will be around these four groups. Let's start by following DDD guidelines and defining our example domain.

From my experience, the most difficult part here is to fight against two impulses: database-driven design (we need to have so-called persistence ignorance) and class-driven design. Don't forget:

The domain is driving the design!

Because we want to go as fast as possible to the code, let's jump directly to the results of DDD analysis! This article is not another DDD article. You can find all about it in many resources, from a functional programming point of view I strongly recommend checking the book Domain Modeling Made Functional.

Knowing the domain

My domain is statistics about property ads. In one sentence I can describe it as follows:

Users will have an API where they can authenticate and check the statistics about property ads (data is coming from the DB), like average price in last week etc.

We split the domain into two subdomains — bounded contexts: Identity and Statistics. Of course, the reality is never as simple as our example. Usually, we have cross-context dependencies, which we don’t have here. You may wonder how the Statistics domain knows which user is logged in, as the user is a part of the Identity context. In my simplified case user is stored in the Elixir Plug Connection after Identity contexts authenticated it.

There is also a caveat about Identity context. By the book, it should not be a part of the domain but included as a whole in the infrastructure. Discusable, but for my example, we can survive with this simplification.

Lets now draw a diagram of what we know about our subdomains:

Diagrams of two contexts (subdomains) as part of my example

As we can see we have commands (with associative data) going in on the left, which are triggering the workflows. On the exit, we have events, that other contexts can subscribe to etc. and which are due to simplification not drawn. In my example, the results of the workflows will simply go back to the “outside world” in a synchronous way.

Domain Layer (domain models + domain services)

For a second let's take a look again at our mini-map. We are still in the application core. We have just defined the domain. Let's go now in the middle of the application core!

There we can find the domain layer, which consists out of domain model and domain services. It's a home for group 1 and group 2 mentioned above. More precise, in the DDD terminology that would be domain models as entities, aggregates, value objects and domain services: domain logic that is reusable between workflows and does not belong to domain models themselves.

It can get confusing, so let’s start drawing the main diagram from what we have just learned:

Diagram of the Domain Layer as the most inner part of the Application Core

There are three things related to domain models that I would like to emphasize and are really really important.

Three rules of the (Elixir) Domain Layer

Besides what I wrote above about the direction of dependencies — Domain Layer depends on nothing (ie. no calls to “outside”), there are three rules that I’ve noticed during my research and that I follow as a must:

  1. Only models can change models. In Elixir, by defining model types as Opaque, we are hiding their inner properties from the outside world. We are exposing just a type, and functions, that's all. Even if the outside is still the Application Core, layers outside of the Domain Model can not access/change properties. Otherwise, the dialyzer will throw an error. That's a big win because we can be sure that only models (and by that, I mean business rules inside) can change models!
  2. Models are not used outside of the Application Core. In other words, models are not leaking, not even inside of the same context. That's why I implement the to_map/1 function for each model, which converts structs to maps. In this way, there is no dependency created! If you want to know more about this, I recommend watching the video The Alchemist’s Code: Bringing More Value with Less Magic.
  3. Use smart constructors to create models. Because of opacity, we can create models only with a constructor function, that takes the input map and builds the model. What does that mean? Whenever we have a model in the Application Core, we can be sure, that was built through the constructor. To push this usage even further — why not parse (validate) the input map? In this way, we can do various business checks on the input, before a model is even created. We should do that even for simple values, like quantities, like it should be a positive number between 1 and 1000.

If you are still questioning yourself why, why, why? The answer is simple, the main pursuit here is:

To have a bounded context that always contains data that can be trusted.

Let's take a look at the contents of one of my model's Credentials, respecting the rules set above:

defstruct [:username, :password]@opaque t :: %__MODULE__{
username: String.t(),
password: String.t(),
}
@spec new(map()) :: {:ok, t()} | {:error, atom()}
def new(params) do
Data.Constructor.struct([
{:username, BuiltIn.string()},
{:password, BuiltIn.string()}],
__MODULE__,
params)
end
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = token_expiration) do
token_expiration
|> Map.from_struct()
end
@spec username(t()) :: String.t()
def username(%__MODULE__{username: u}), do: u
@spec password(t()) :: String.t()
def password(%__MODULE__{password: p}), do: p

At the top, I’m defining the domain model struct and then the type as opaque. After that, I’m implementing a smart constructor, by doing simple parsing, ie. requiring username and password to be non-empty strings. For smart constructing and parsing, I’m using an elixir library called data.

It contains basic built-in parsers for checking types. It also contains tools to create custom parsers in a very neat and readable way. I highly recommend the read about parsing in general: Parse, don’t validate — Elixir edition.

Lower I’m implementing to_map/1 function to convert the model to a simple map for the reasons mentioned above. Besides that, I’ve coded also getters – although I could rely only on to_map/1 function. Sometimes for better readability, I use simple getters. For these, I could develop macro, but that is a topic for another article.

It might be seen as too much ceremony and too verbose. But to be able to trust the model all the time beats all the verbosity. Think about it like this: how many times do you wish you had input and output validation of your data?

Now that I’ve covered the Domain Layer, let's go one step out. Remember, we are still in the Application Core and we still have two groups (group 3 and group 4) to cover. We will continue in the second part of the series. You are welcome to continue!

--

--