Patterns should fit nicely, like playing blocks
This post is part of the Scalable Frontend series, you can see the other parts here: “#1 — Architecture” and “#3 — The State Layer”.
Let’s continue our conversation about frontend scalability! In the last post, we discussed architecture fundamentals in frontend applications, but only conceptually. Now we’re going to get our hands dirty with actual code.
Common patterns
How do we implement architecture as mentioned in the first post? What’s different compared to what we’re used to do? How can we combine all of that with dependency injection?
There are recurring patterns in frontend applications, no matter what library you’re using to abstract the view or to manage the state. Here we’re going to talk about some of them, so buckle up your seatbelt!
Use cases
We chose use cases as the first pattern because architecture-wise they are the means by which we interact with our software. Use cases tell what our application does at a high-level; they are the recipes for our features; the main units of the application layer. They define the application itself.
Also often called interactors, the use cases hold the responsibility of performing the interaction between the other layers. They:
Are called by the input layer,
Apply their algorithm,
Make the domain and infrastructure layers interact without caring about how they work internally, and,
Hand the result status back to the input layer. The result status indicates whether the use case was successful or failed because of an internal error, a failed validation, a pre-condition, etc.
Knowing the result status is useful because it helps to determine what action to emit for an outcome, thus allowing for richer messages in the UI, so that the user knows what went wrong in a failure situation. But there’s an important detail: the logic for the result status should be inside the use case, not the input layer — since it’s not the input layer’s responsibility to know that. It means the input layer should not receive a generic error object from the use case and resort to if
statements to find out the reason of the failure — like checking an error.message
attribute or instanceof
to query the class of the error.
Which takes us to the tricky fact: returning promises from use cases might not be the best design decision because promises only have two possible outcomes: success and failure, requiring us to resort to conditionals to discover the reason of the failures inside catch()
statements. Does it mean we should skip promises in our software? No! It is totally OK to return promises from other parts of our code, like actions, repositories, and services, as long as the input layer remains ignorant about that. A simple way to overcome this limitation is to have a callback for each possible result status of the use case.
Another important characteristic of use cases is that they should respect the boundaries between layers by not knowing what entry point is calling them, even in the frontend where there’s a single entry point. It means that we should not touch browser globals, DOM-specific values, or any other low-level object inside the use cases. For example: we should not receive an instance of an <input />
element as a parameter and then read its value; the input layer should be the one responsible for extracting out this value and passing it to the use case.
Nothing better to make a concept explicit than an example:
export default ({ validateUser, userRepository }) => async (userData, { onSuccess, onError, onValidationError }) => {
if(!validateUser(userData)) {
return onValidationError(new Error('Invalid user'));
}
try {
const user = await userRepository.add(userData);
onSuccess(user);
} catch(error) {
onError(error);
}
};
const createUserAction = (userData) => (dispatch, getState, container) => {
container.createUser(userData, {
// notice that we don't add conditionals to emit any of these actions
onSuccess: (user) => dispatch(createUserSuccessAction(user)),
onError: (error) => dispatch(createUserErrorAction(error)),
onValidationError: (error) => dispatch(createUserValidationErrorAction(error))
});
};
Notice that in userAction
, we don’t make any assertion over the response of the createUser
use case; we trust that the use case will call the correct callback for each result. Also, even though the values inside the userData
object come from HTML inputs, the use case doesn’t know anything about that. It just receives the extracted data and passes it forward.
That’s it! The use cases shouldn’t do more than that. Can you see how easy it is to test them now? We can simply inject mock dependencies that behave as we want, and test if our use cases call the correct callback for each situation.
Entities, value objects, and aggregates
Entities are the core of our domain layer: they represent the concepts that our software deals with. Say we’re building a blog engine application; in that case, we’ll probably have a User
entity, an Article
entity, and even a Comment
entity if our engine allows so. Entities are therefore simply objects that hold the data and the behavior of these concepts, without taking the technology into consideration. Entities shouldn’t be seen as models or implementations of the Active Record design pattern; they don’t know anything about databases, AJAX, or persistence at all. They just represent the concept and the business rules around that concept.
So, for example, if a user of our blog engine has age restrictions when commenting on an article about violence, we would have a user.isMajor()
method that would be called inside article.canBeCommentedBy(user)
, in such a way as to keep the rule for age classification inside the user object and the rule for age restriction inside the article
object. The AddCommentToArticle
use case would be the one to pass the user instance to article.canBeCommentedBy
, and the use case would be the one to perform the interaction between them.
There’s a way to identify if something is an entity in your codebase: if an object represents a domain concept and it has an identifier attribute (an id, a slug or a document number, for example,) it is an entity. The existence of this identifier is important because it is what differentiates an entity from a value object.
While the entities have an identifier attribute, the value objects have their identities defined by the value of all its attributes combined. Confusing? Think about a color object. When representing a color with an object, we usually don’t give this object an id; we give it values of red
, green
and blue
, and it’s these three attributes combined that identify this object. If we change the value of the red
attribute, we can now say that it represents another color, but the same doesn’t happen with a user identified by an id. If we change the value of the name
attribute of a user but keep the same id
, we say it’s still the same user, right?
At the beginning of this section, we said that it’s common to have methods within the entities with the business rules and behaviors for that given entity. But in the frontend, it’s not always possible or good to have business rules as methods of entity objects. Think about functional programming: we don’t have instance methods, or this
, or mutability — and it’s a paradigm that plays very well with the one-way data-flow using plain JavaScript objects instead of instances of custom classes. Does it make sense to have methods in the entities when using functional programming then? Certainly no. So how do we create entities with such limitations? We go by the functional way!
Instead of having a User
class with a user.isMajor()
instance method, we’ll have a User module with a named export called isMajor(user)
, which takes an object with user attributes and treats it as if it was the this
from a User
class. The argument doesn’t need to be an instance of a specific class, provided that it has the same attributes a user would have. This is important: the attributes (which are the expected arguments for the User
entity) should be formalized somehow. You can do it in pure JavaScript with factory functions or more explicitly using Flow or TypeScript.
Shall we see a before and after to make it easier to understand?
// User.js
export default class User {
static LEGAL_AGE = 21;
constructor({ id, age }) {
this.id = id;
this.age = age;
}
isMajor() {
return this.age >= User.LEGAL_AGE;
}
}
// usage
import User from './User.js';
const user = new User({ id: 42, age: 21 });
user.isMajor(); // true
// if spread, loses the reference for the class
const user2 = { ...user, age: 20 };
user2.isMajor(); // Error: user2.isMajor is not a function
// User.js
const LEGAL_AGE = 21;
export const isMajor = (user) => {
return user.age >= LEGAL_AGE;
};
// this is a user factory
export const create = (userAttributes) => ({
id: userAttributes.id,
age: userAttributes.age
});
// usage
import * as User from './User.js';
const user = User.create({ id: 42, age: 21 });
User.isMajor(user); // true
// no problem if it's spread
const user2 = { ...user, age: 20 };
User.isMajor(user2); // false
When dealing with state managers like Redux, the easier you can make to favor immutability the better, so not being able to spread objects to create shallow copies is not a good thing. Using the functional approach will enforce decoupling and we’ll still be able to spread objects.
All these rules apply to value objects, but they also have another importance: they help make our entities less bloated. It’s common to have a lot of attributes not directly related to each other in entities, which may be a sign that we can extract some of them to value objects. For example, let’s say we have a Chair
entity with id
, cushionType
, cushionColor
, legsCount
, legsColor
, and legsMaterial
. Notice that cushionType
and cushionColor
are not related to legsCount
, legsColor
, or legsMaterial
, so after extracting some value objects, our chair would be reduced to three attributes: id
, cushion
and legs
. Now we can continue adding attributes to cushion and legs without making the chair even more bloated.
Before extracting value objects
After extracting value objects
But just extracting value objects from an entity won’t always be enough. You’ll notice that, more often than not, there will be entities associated with secondary entities where the main concept is represented by the first entity, which depend on these secondary entities to be a whole, and that the existence of these secondary entities alone doesn’t make sense. There’s certainly some confusion in your head right now, so let’s clear it out.
Think about a shopping cart. A shopping cart can be represented by a Cart entity, which will be composed of line items, which in turn are also entities, since they have their own ids. Line items should only be interacted with through the main entity, the cart object. Want to know if a given product is inside the cart? Call the cart.hasProduct(product)
method instead of reaching out to the lineItems
attribute directly like cart.lineItems.find(...)
. This kind of relationship between objects is called an aggregate, and the main entity of the given aggregate (in this case, the cart object) is called the aggregate root. The entity that represents the concept of the aggregate and all of its components can only be accessed through the cart, but it’s OK for the entities inside the aggregate to reference objects from the outside. We can even say that, in cases where a single entity alone is able to represent a whole concept, that entity is also an aggregate composed of a single entity and its value objects, if any. So when we say “aggregate”, from now on you must interpret it as proper aggregates and single-entity aggregates as well.
There’s no access from the outside to the internal entities of the aggregate, but the secondary entities can access things from outside of the aggregate, like the products
Having entities, aggregates, and value objects well-defined in our codebase and named after how the domain experts refer to them can be very valuable (no pun intended). So always pay attention to whether something can be abstracted with them before throwing the code somewhere else. Also, be sure to understand entities and aggregates because it’s going to be useful for the next pattern!
Repositories
Did you notice we didn’t talk about persistence yet? It’s important to think about it because it enforces something that we spoke since the beginning: persistence is an implementation detail, a secondary concern. It doesn’t matter where you persist stuff in your software, as long as the part responsible for handling it is reasonably encapsulated and doesn’t affect the rest of your code. In most layer-based architectures, this is the responsibility of the repository, which lives inside the infrastructure layer.
Repositories are objects used to persist and read entities, so they should implement methods that make them feel like collections. If you have an article
object and you want to persist it, you’ll probably have an ArticleRepository
with an add(article)
method which takes the article as a parameter, persists it somewhere, and then returns an article copy with additional persisted-only attributes like the id.
I said that we would have an ArticleRepository
, but how do we persist other objects? Should we have a different repository to persist users? How many repositories should we have, and how granular should they be? Calm down there, the rule is not hard to grasp. Do you recall aggregates? That’s where we cut off the line. The rule of thumb is having a repository for each aggregate of your codebase. We can also create repositories for secondary entities, but only if needed.
Ok, ok, it sounds a lot like backend talk. What do repositories do in the frontend then? We don’t have databases in there! And here’s the catch: stop associating repositories with databases. Repositories are about persistence as a whole, and not only about databases. In the frontend, repositories deal with data sources such as HTTP APIs, LocalStorage, IndexedDB and so on. In the previous example, our ArticleRepository#add
method takes an Article
entity as input, converts it to the JSON format the API expects, makes an AJAX call to the API, and then maps the JSON response back to an instance of the Article
entity.
It’s nice to notice that, for example, if the API is still in development, we can emulate it by implementing an ArticleRepository
called LocalStorageArticleRepository
, which interacts with the LocalStorage instead of the API. When the API is ready, we then create another implementation called AjaxArticleRepository
, thus replacing the LocalStorage implementation — so long as both of them share the same interface and are injected with a generic name that doesn’t reveal the underlying technology, like articleRepository
.
We use the term interface here as to represent the set of methods and attributes that an object should implement, so don’t confuse it with graphic user interfaces (a.k.a. GUIs). If you’re using pure JavaScript, the interfaces will be only conceptual; they’ll be imaginary, since the language doesn’t support explicit declaration of interfaces, but they can be explicit if you’re using TypeScript or Flow.
Services
This one is not the last pattern just for a coincidence. It is here precisely because it should be seen as the “last resource”. When you can’t fit a concept in any of the previous patterns, only then should you consider creating a service. It is very common to see codebases where any piece of reusable code is thrown in a so-called “service object”, which is nothing but a bucket of reusable logic with no encapsulated concept. Always be aware of that and don’t let this happen in your codebase, and resist the urge to create services instead of use cases because they are not the same thing.
In short: services are objects that implement procedures which don’t fit in your domain objects. For example, payment gateways.
Let’s imagine for a second that we’re building an e-commerce and we need to interact with an external API of the payment gateway to fetch the authorization token of a purchase. The payment gateway isn’t a domain concept, so it’s a perfect fit for a PaymentService
. Add methods to it that won’t reveal technology details such as the format of the API response, and then you have a generic object to perform the interaction between your software and the payment gateway, nicely encapsulated.
And that’s it, no secret here. Try to fit your domain concepts in the aforementioned patterns, and if they don’t work, only then consider a service. It counts for all the layers of your codebase!
File organization
Many developers misunderstand the difference between architecture and file organization, thinking that the latter defines the architecture of an application. Or even that with good organization, the application will scale well, which is totally misleading. Even with the most perfect file organization, you can still have performance and maintainability issues through your code base, so there’s the last topic of this post. Let’s demystify what organization really is, and how we can use it in conjunction with architecture to achieve a readable and maintainable project structure.
Basically, organization is how you visually separate the parts of your app, while architecture is how you separate it conceptually. You can perfectly well keep the same architecture and still have multiple options to choose from when picking an organization scheme. It’s a good idea, however, to organize your files as to reflect the layers of your architecture and favor the readers of your codebase, so that they understand what happens just by looking at the file tree.
There is no perfect file organization, so choose wisely according to your taste and needs. There are two approaches, however, particularly useful to highlight the layers discussed in this article. Let’s see each of them.
The first one is the most simple and consists in dividing the root of the src
folder in layers, and then in the concepts of your architecture. For example:
.
|-- src
| |-- app
| | |-- user
| | | |-- CreateUser.js
| | |-- article
| | | |-- GetArticle.js
| |-- domain
| | |-- user
| | | |-- index.js
| |-- infra
| | |-- common
| | | |-- httpService.js
| | |-- user
| | | |-- UserRepository.js
| | |-- article
| | | |-- ArticleRepository.js
| |-- store
| | |-- index.js
| | |-- user
| | | |-- index.js
| |-- view
| | |-- ui
| | | |-- Button.js
| | | |-- Input.js
| | |-- user
| | | |-- CreateUserPage.js
| | | |-- UserForm.js
| | |-- article
| | | |-- ArticlePage.js
| | | |-- Article.js
It’s very common to see folders like components
, containers
, reducers
, actions
, and so on, when working with React and Redux using this kind of organization. We preferred to go one step further and group similar responsibilities within the same folder. For example, our components and containers will all be inside the view
folder, and the actions and reducers will be inside the store
folder because they follow the rule of gathering together the things that change for the same reason. The following are the standing points of this organization:
You should not have folders to reflect technical roles like “controllers”, “components”, “helpers” and so on;
Entities live inside the
domain/<concept>folder
, where “concept” is the name of the aggregate in which the entity lives, and is exported through thedomain/< concept>/index.js
file;When a unit may fit in two different concepts, choose the one where the given unit wouldn’t exist if the concept didn’t exist;
It’s OK to import files between concepts of the same layer, as long as it doesn’t cause coupling.
Our second option consists in separating the root of the src
folder by features. Let’s say we are dealing with articles and users; in that case, we will have two feature folders to organize them, and then a third folder for common things like a generic Button
component, or even a feature folder only for UI components:
.
|-- src
| |-- common
| | |-- infra
| | | |-- httpService.js
| | |-- view
| | | |-- Button.js
| | | |-- Input.js
| |-- article
| | |-- app
| | | |-- GetArticle.js
| | |-- domain
| | | |-- Article.js
| | |-- infra
| | | |-- ArticleRepository.js
| | |-- store
| | | |-- index.js
| | |-- view
| | | |-- ArticlePage.js
| | | |-- ArticleForm.js
| |-- user
| | |-- app
| | | |-- CreateUser.js
| | |-- domain
| | | |-- User.js
| | |-- infra
| | | |-- UserRepository.js
| | |-- store
| | | |-- index.js
| | |-- view
| | | |-- UserPage.js
| | | |-- UserForm.js
The standing points of this organization are basically the same as the first one. For both, you should keep the dependencies container in the root of the src folder.
Again, these options may not fit your needs, and therefore might not be the perfect organization for you. So take the time to play and move files and folders around until you achieve a result that makes it easier to find the artifacts you need. This is the best way to find out what works better for your team. And be aware that just separating code into folders won’t make your application easier to maintain! You got to keep the same mindset while separating the responsibilities within your code.
Coming next
Wow! Quite a few content, right? It’s OK, we spoke about a lot of patterns here, so don’t push yourself to understand all of this in a single read. Feel free to re-read and check the first post of the series and our examples until you feel more comfortable with the outline of the architecture and its implementation.
In the next post, we’ll also talk about practical examples, but focusing totally in state management.
In case you want to see a real implementation of this architecture, check out the code for this example blog engine application. Remember that nothing is written in stone, and that there will be patterns we’re still to talk about in the upcoming posts.
Recommended links
Written by Talysson de Oliveira and Iago Dahlem Lorensini.
We want to work with you. Check out our "What We Do" section!