Skip to main content

Android Actual Clean Architecture (AACA)

by Adrian Tache, 01.05.2024

The Domain (Entity)

What exactly is an entity?

TL;DR

An entity is the core logic that whatever you're modelling would be doing in a real life scenario. It is responsible for its state and logic and it also holds the data that is required to perform that logic, or to communicate its results to outside components. It knows nothing about the app, UI, etc.

The entity is the core of this architecture, and the component you should spend the most time with. In simple terms, this is the component that represents the real-world business logic of whatever it is that you are doing. It will contain all the logic to perform those business actions, as well as the data it needs to do so. It will hold its own state, and will mutate based on every operation that is performed to it.

To every other part of the app, the entity is a black box, usually inaccessible, other than through the methods and data it provides, and this access is coordinated by the use cases.

There will usually be multiple entities in an app, each the representation of a real-world object, process or concept, and they will either interact with each other, or will be coordinated by the use cases.

It is far beyond the scope of this document how to generate them, but please visit the Resources section below for some books that describe Domain-Driven Design and how to best transform real life activities into these classes.

Why do we need it?

TL;DR

Entities are used to hide away business logic, protecting it from other layers and thus making it more robust, easier to test and easier to think about.

The core of this architecture is the business logic, which is the purpose of the app, if you abstract away the fact that it is an app. For example, if you're building a banking app, the business logic would be all the interactions between the user and the bank, whether it's checking their balance, or making a payment, and so on.

Its purpose is to hide away this logic from any influences other than the pure transactions it represents, thus keeping it as simple to work with, and therefore easy to test and difficult to break, since it will be obvious if a real life transaction is doing things that wouldn't happen in real life. This helps by keeping its complexity low, and its functionality predictable, so even as our app grows, we have a gut feeling as to where everything belongs, and we can usually test it with reality.

The entity gives structure to our logic, and helps hide this logic via encapsulation. For example, if a user enters an amount for a payment, our Payment entity will validate this amount and change some of its properties, but for the outside world this will be transparent, just a method to update an amount, and a resulting state. Having things built this way removes a lot of complexity from use cases, and lets processing of data be atomic, and linked to the context in which it's occurring. If we didn't have entities, use cases would balloon in size, and their complexity would increase tremendously, since their methods would perform operations on multiple objects, and it would be more and more difficult to understand what's happening inside them.

Real life examples

Let's start with the simplest, most common example: a to-do app. The entity at the core of any to-do app would be, of course, the to-do. This will contain its text and whether it's done as data, as well as some methods to interact with it. All in all, it would probably look something like this:

domain/entity/ToDoEntity.kt
data class ToDoEntity(
val text: String = "",
val isDone: Boolean = false,
val id: String = UUID.randomUUID().toString(),
) {
val canBeDone: Boolean = text.isNotBlank()

val isValid = text.isNotBlank()

fun editText(text: String): ToDoEntity {
return ToDoEntity(
text = text,
)
}

fun flipDone(): ToDoEntity? {
if (!canBeDone) return null

return this.copy(
isDone = !isDone
)
}
}

There are a few things to note in this example:

  1. The class is a data class, and has default values, so that it is easy to create a new to-do item, since there are no validations for this. Its constructor parameters are immutable, but publicly visible, just like a to-do note on paper.
  2. There are a couple of parameters that are auto-generated based on the data of the class. I recommend leaving these out of the constructor, since they are derived from the data and thus should not be set by anyone instantiating the class, since, even with a private constructor, this can happen via the copy method.
  3. One of these parameters is isValid, and it's something I frequently include in my entities which receive user input. Often, we want to perform validation on items, and it's very useful to have this indication that the entity is in a valid state. In this case, my reasoning was that we wouldn't want to save a to-do that is blank, so we consider it invalid. It will then be up to the use case to decide what happens in this invalid case.
  4. The other parameter is canBeDone, which is a utility boolean whose purpose is to inform any consumer of this class if an operation can be done to impact its isDone field, and thus to avoid illegal operations. More on this at point 7 below.
  5. The class exposes a number of methods to allow a user to interact with it. Like in the case of a to-do on a piece of paper, we can change its text, or we can mark it as done. The entity does not care at all how this process is performed. In the case of changing the text, all it does is take the new text and replace the old.
  6. As you can see, the editText method does not mutate the text field, rather it creates a brand new object with the modification that is required. This will automatically update the class variables that are generated, and it will be used to replace the old object, with the expired state.
  7. The second method which allows user interaction, flipDone, is slightly different, in that it returns a nullable version of the entity. This enables for validation of its inputs, and not updating the state if this validation fails.
  8. Another important feature of flipDone is that it takes no input, meaning that we rely on the state of the entity rather than the UI to be the source of truth. Whenever the user doesn't need to give us a brand new input, we should always rely on the entity state, so that if there is a desync, it is our business logic which ensures consistency, and it is the UI's responsibility to keep things synchronized to the state.
  9. While it has nothing to do with business per se, it is a good idea to give entities that are either displayed as a list, or are saved as one, an ID to help the other layers work with them easier. This is a case of breaking the architecture a bit, by being aware of this, but it would otherwise be quite complex to work with it (n.b. if you have a better idea of doing this, please use the contact form to let me know!)

TODO more complex example

Rules

1. Entities shouldn't know about your app

The purpose of entities is to have a close representation of your real life logic written as code. As such, they should be as broad and as unrelated to the final implementation of your app as possible. Entities exist to decrease complexity, so the more they exist to mirror clear, real-life concepts, the easier they are to work with, especially over time, and this cannot happen if they start concerning themselves with details related to your app.

2. Entities should be protected

While it may be tempting to pass entities to other layers directly, or to interface with them directly, there is only one other component that should be interacting with entities, and that is the Use Case (directly or via a Mapper). This way we can ensure that entities remain unaware about the rest of the app, and that their functioning is as robust as possible. Of course, entities can and should interact with one another.

3. Entities should be immutable

Most operations with an entity should return a new copy of it, or null in the case of validation failures that do not trigger an error. This is in order to keep behaviour predictable, atomic and just avoid the pitfalls of having an object mutate inside such a deep architecture.

4. Entities should expose all information about themselves

In general, entities should be treated as black boxes by every part of the app, including the Use Cases. This means that, while use cases will know about their public methods and fields, they should never reach in to combine information by themselves. Instead, the entity should consider what information could be needed from it, by any layer, and simply provide it as a field, already computer. In my experience, I've seen that it is very useful to have an isValid boolean inside the entity, to give other entities or use cases

5. Keep entities simple

One of the wonderful benefits of entities is the fact that, since they are based on real life items or concepts, they can be broken down further into smaller bits and pieces and still keep their meaning. For example, an entity that describes a payment can contain other entities that describe details about it, such as an address entity, a payment amount entity (which can contain its own payment currency entity, or not), etc.

6. Give entities a default constructor, if it makes sense

In general, entities will be instantiated and then receive interactions from the use case. As such, it generally makes sense to build an empty entity, so that it is instantiated and ready to receive these interactions, in a predictable initial state. You must, however, be careful not to trigger any errors from your validations when the entity is in this state, and if this is difficult, a simpler solution would be to simply make it null in the initial state.

Testing

TL;DR

Entities are built to be easy to test, so just use them as you would normally and ensure the outputs are what you expect.

Entities are self-contained, immutable black boxes, and therefore are wonderfully easy to test. All you need to do is to instantiate an entity (usually via its empty constructor), give it some interaction and then check its data. To make things easier, I highly recommend building test fakes when you're building your entities, since you are already in the mental context required to build a reliable fake. Then, in order to test an entity, you can replace all its sub-entities with fakes, and just ensure that the behaviour is what you want. Or, of course, you can just use the real entity, if you want it to still perform all its validations etc.

One huge benefit of having entities (and other components in this architecture, for this matter) be black boxes, is that testing is much easier, since we can focus on inputs and outputs, without caring too much about the actual implementation inside the entity itself. Thus, if that implementation changes, the test don't really need to change, or they need to change in a very simple way to reason about or update.