Domain-driven design in Android — Part 3

David Rawson
7 min readMar 4, 2021

Layer cake

In previous entries in the series, we learned the importance of a model and an ubiquitous language. We also understand that our app has to deal with some activity of interest to the user — the domain. A layered architecture is a tool for isolating this domain. This is because there is a world of implementation detail outside the domain. Detail like talking to the system via Intents, writing to a SQL Lite database etc. We don’t want everything crowded together — we would like to see the model as a system in a single glance.

Consider a “god object” 2000-line monster Fragment in Android. It probably has a mix of UI code, calls to the database or network, and real business rules for how the app should function. This mix is not ideal. As DDD explains:

We should not be forced to pick [the model] out of a much larger mix of objects, like trying to identify constellations in the night sky”.

Photo by Andy Holmes on Unsplash

But first, a caveat.

Layered architecture is not always the right choice

While layered architecture will isolate the domain, you pay a price in complexity of mapping one layer to another. You will need to write code to covert from DTOs or database objects to domain objects, and from domain objects to ViewState or ViewProps. Sometimes you end up with duplication, and although Uncle Bob explains this duplication as accidental, it can be hard to justify.

The question that needs to be asked is “does this make the codebase easier to understand?” In the case of an extremely complicated domain, the benefit from isolating the domain to make the business rules apparent is obvious. In a simple domain, the boilerplate of mapping outweighs the benefit of isolating a domain and it may be best to avoid a layered architecture.

Believe it or not, DDD advocates this pattern using the term “Smart UI.” It could be could be analogous to “smart Activities” or “smart Fragments” in Android — UI classes that contain business logic. For a small project, or for a team not yet conversant in the concepts of layered architecture, this may be a suitable choice, despite the howls of protest from architecture purists:

Therefore, when circumstance warrant:

Put all the business logic into the user interface. Chop the application into small functions and implement them as separate user interfaces, embedding the business rules into them. Use a relational database as a shared repository of the data…

Bearing this in mind, it’s best to think of layers as tools in a toolbox that can be brought into your codebase to solve a particular problem, rather than architectural mandates.

What is layered architecture?

In layered architecture, we separate the concerns of data sourcing and presentation into distinct layers. In other words, how we persist and how we show are abstracted away, leaving a layer that is simply business logic. Each layer can only depend on elements from the same layer, or can receive from those below.

This concept is implicit in the various architectures that have been advocated over the years: MVP, MVVM, MVI and their variants. Because the distinction between the different layers can be quite abstract, let’s try and understand it through some counterexamples that show what happens when we mix the layers together and break layered architecture.

Data layer leaking into the domain

We will take our ViewModel as representing the domain layer, since it’s not directly rendering views on screen (presentation) and it’s the sphere in which the business rules are defined. For Android developers, the business rules are the logic for how the app should behave — whether I should be able to move to the next screen given the current state of the app and so on. Some talk of a separate application layer specifically for this kind of logic, but for now we take a broad interpretation of “domain.”

Now consider the following example:

Here the ViewModel is directly depending on a Room TasksDao. This means the ViewModel “knows” it is loading from a Room database. While this may be okay in a simple app, in layered architecture, this is an example of “data layer leaking into the domain layer.”

Why? The ViewModel shouldn’t know the implementation details of the layer beneath. It should neither know nor care that data is coming from Room, SQLDelight, Realm, SharedPreferences, or from over the network. In another way, if we had to change from Room to SQLDelight we would have to change the ViewModel, meaning our ViewModel was not isolated from the layers below it.

Similarly, if the ViewModel had to manage between offline and online states, choosing the data source in dependence on these, we would lose clarity on the domain. This logic of manipulating data sources would interfere with understanding critical functionality of the ViewModel that determines how the app should behave.

All of this explains the need to load from a repository — a domain-layer abstraction over a data source. Having the ViewModel load from a repository prevents this leak of information from the layer below. The ViewModel can now concentrate on stating domain logic:

Relation to the ubiquitous language

To illustrate this distinction, let’s imagine a new requirement for obtaining the tasks over the network comes along. Compare the conversations with the product owner between the two versions:

Version 1 — AddEditTaskViewModel directly depends on the DAO

Product owner: Tasks are currently only stored on the app. What would we have to do in order to get tasks over the network?

Dev: Well we’d have to add logic inside the ViewModel to switch depending on the network state, then add a dependency on a Retrofit service to make the API call.

Version 2 — AddEditTaskViewModel depends on a repository

Product owner: What would we have to do in order to get tasks over the network?

Dev: Well we’d have to add an additional data source to the TasksRepository: a NetworkDataSource.

Notice how the first conversation is mired in implementation details, a ViewModel, a Retrofit service etc. The second conversation uses the more generic concepts of a domain model — repository, data source etc. These are concepts generic enough to fold into the ubiquitous languages.

Another example

Similarly, not wanting to leak from one layer to another would motivate us to create a separate DTO from a domain object, or maintain two separate classes for a database object and for a domain object.

Again, we don’t want the decisions we have taken in how we store our data to determine how we model our domain. Consider a Task class which we use to encode app business logic, but using the Realm ORM for persistence:

Realm forces you to use models that are open. This will prevent you from making the model a Kotlin data class, and will leave your model open to sub-classing. If this is undesirable, we could consider extracting a separate domain object and mapping between the Realm model and our domain object:

Note that we incur the cost of maintaining the extra class as well as a mapping between layers in order to reap the benefits of a closed data class on which we can encode business rules cleanly.

When to incur the cost of a separate data layer

  • Your ViewModels start to deal with the complexities of managing multiple data sources.
  • You have met a limitation in your persistence framework that has harmed the way you encode business rules.
  • A single class is pulled in two directions from being used with two different data sources e.g., when it is decorated with annotations for both a persistence framework and a serialization framework.
  • Persistence concerns have made for difficulties in testing (e.g., by requiring you to use Robolectric for testing).
  • The objects you get over the API are not a good fit for your Android app (e.g., from having a one-size-fits-all API).

Domain layer leaking into the presentation layer

Consider the following example:

Our Adapter takes a raw Task, and gives it a color inside the ViewHolder. The color for the Task is something we might want to test. But the logic for determining the color is now inside the ViewHolder. We’d prefer to add that logic somewhere else. What about inside Task itself?

If we do this, we have started to add Android-specific metadata like ColorRes inside our domain models. This might not be so bad, but it might not make sense to have the color baked into the Task itself. What if different screens need to use different color schemes? And what if the color also depends on the position of the item in the list? We’d prefer to have logic for determining the color of a Task inside a ViewModel. That way we can test the logic easily.

In a pedantic layered architecture, the data we surface to Fragments and Activities (through, for instance, the type parameters for LiveData) should only be bags of primitives, not domain objects like a Task. Sometimes these surface-able objects are called ViewProps or ViewState, but no matter what we call them we should be careful to stop the Fragments, Activities, and ViewHolders from knowing too much about our domain, since changes to the way the Fragments and so on display their data shouldn’t taint our model. One solution to consider here is to map our domain objects into presentation-layer objects:

Now the logic is in the ViewModel and can be tested:

Since the methods in Adapters are named getItem, one way of remembering the distinction between these layers is that an Adapter adapts items, not domain objects. As stated earlier, you could also call these objects “ViewProps” or “ViewState.”

Note here that the distinction between domain and presentation is not as sharp as between domain and data. In other words, your app is less likely to need a separate presentation layer than a separate domain layer. However, as above, we can enumerate some problems that introducing a presentation layer can solve:

When to consider a separate presentation layer

  • Complex logic for presentation of domain objects starts to be written inside RecyclerView.ViewHolders, Activities, or Fragments.
  • The way you present an object starts to interfere with how you model the object.
  • You’d like some unit test coverage for the way your objects are presented.
  • You are using a framework like Epoxy that forces you to make this distinction.

Continued in part 4

--

--