3.
Domain Layer
Written by Ricardo Costeira
Having your business logic smeared throughout your app is a recipe for disaster. In time, things will get messy:
- Code will become hard to find.
- You’ll start reimplementing logic by accident.
- Logic will get more and more coupled to the code that calls for it.
- Your code will have mixed responsibilities. As the project grows, it’ll become harder to change.
That’s why it’s a good practice to decouple your business logic. A nice way to do that is to implement a domain layer.
In this chapter, you’ll learn:
- What a domain layer is, why you need it — and when you don’t.
- The difference between domain entities and value objects.
- How to determine which entities and/or value objects to model.
- Common issues in domain modeling.
- The role of a repository in the domain layer.
- What you should test.
Although use cases are also part of this layer, you won’t implement any for now because they’re tailored for features.
What Is a Domain Layer?
The domain layer is the central layer of your app. It includes the code that describes your domain space along with the logic that manipulates it. You’ll probably find at least the following objects in every domain layer you work with:
- entities: Objects that model your domain space.
- value objects: Another kind of object that models your domain space.
- interactors/use cases: Logic to handle entities and/or value objects and produce a result.
- repository interfaces: Define contracts for data source access.
This layer encompasses the business logic of the app. Your business logic is one of the most important parts of your app, as it defines how the app works. The less you mess with it, the better! That’s why the domain layer shouldn’t depend on other layers.
For example, imagine you change your data layer by migrating from REST to GraphQL. Or you change your presentation layer by migrating the UI to Jetpack Compose. None of those changes have anything to do with the business logic. As such, they shouldn’t affect the domain layer at all.
Do You Really Need a Domain Layer?
Whether a domain layer is necessary is a source of debate in the Android community. Some people argue that it doesn’t make sense to have one in Android apps. Google, in their architecture guide, also mark the domain layer as optional.
At a high level, a lot of Android apps follow the same simple pattern. They:
- Get data from a data source.
- Show the data in the UI.
- Update the data source with new data.
From a layered architecture point of view, it seems like a data and a presentation layer would be enough!
And they are — for the app to work, at least. You just need to pass data between the layers, maybe add some logic in your ViewModel
s to handle the data, and off to the Play Store it goes.
You have a working app, but you forgot about something — or someone — really important. You forgot about you.
Having a domain layer is a way of protecting yourself as a developer. Sure, it can seem like unnecessary, redundant work, but it pays off in the long run by:
- Keeping your code clean and easy to maintain by focusing the business logic in one layer only, and decoupled from data source code. Single responsibility code is easier to manage.
- Defining boundaries between code that implements app logic and code that has nothing to do with that logic, like UI or framework code. Given how fast the Android framework changes, this separation is critical.
- Easing the onboarding of future developers, who can study the layer to understand how the app works.
If you’re working with a small codebase, it’s true that having a domain layer might be over-engineering. If all your app does is transform API data to show it to the user, then you can just use the repository for those transformations and your code will work, nice and simple. Yet, small apps are becoming increasingly rare. Even for apps that are small feature-wise, code gets really complex, really fast. It might seem like over-engineering at first, but sooner rather than later, it’ll turn out to be a life- and sanity-saving design decision.
In the end, it’s something you should consider, or discuss with your team. It might even make sense to start without a domain layer, and then add one in the future if needed.
At this point, PetSave has a relatively small codebase. As you go through the book and add more code, however, you’ll start to see how the domain layer really shines. You’ll see how nice it is to have a clear separation of concerns, which in turn allows for easily tested logic.
But that’s in the future. For now, it’s time to add your first domain entities.
Creating Your Domain Model
Use Android Studio and open the PetSave project you find in the starter folder in the material for this chapter, and expand the common.domain package. You’ll see two other packages inside:
- model: Where all entities and value objects live.
- repositories: Where you’ll find any repository interfaces.
You’ll come back to the repositories later. For now, focus on the model package. As you start exploring it, you’ll notice that it already has quite a few files inside.
Entities and Value Objects
Now’s a good time to establish the difference between entities and value objects. Expand the common.domain.model.organization package and open Organization.kt. Focus on the first data class:
data class Organization(
val id: String,
val contact: Contact,
val distance: Float
)
This class represents the organization entity. It has an id
that identifies it and a few properties that describe it. Look at that Contact
, and you’ll notice that it doesn’t have an ID.
data class Contact(
val email: String,
val phone: String,
val address: Address
)
This is what distinguishes entities from value objects:
- Entities have an ID that allows you to tell them apart. Their properties can change, but the ID always remains the same.
- Value objects describe some aspect of an entity. They don’t have IDs, and if you change one of their properties, you create a new value object. For this reason, they should always be immutable.
Note: The concept of entities and value objects comes from Domain Driven Design. Although the distinction between them goes deeper, the key thing to remember is this: Identity is important for entities.
As long as the ID remains the same, entities’ properties can change. However, the Organization
entity only has immutable properties.
That’s because it’s good practice to favor immutable objects until you need to make them mutable. This does wonders for avoiding bugs that stem from mutable objects. A very common example is mutable object handling with asynchronous code.
What Should You Model?
In a real-world situation, you wouldn’t have to think about what to model at this point. You would have info from stakeholders and project owners and decisions from meetings that would guide you on what to implement.
Frequently, apps are built to support a pre-existing business. In these cases, the domain model already exists somewhere — typically in the back end. Therefore, reproducing the back end’s domain model is usually enough.
This last option is actually the case for PetSave. Most of the domain is based on the petfinder API. And why not? They already have a working back end with a matching front end. Plus, it’s the only API PetSave will use in the foreseeable future.
More often than not, the domain model entities end up being manifestations of the domain’s names. For PetSave, you know the domain has to do with animal adoption and care. You adopt animals from organizations that care for them. Animals and organizations seem like a starting point!
Adding the Animal Entities
As you’ve seen, Organization
already exists. Next, you’ll add the Animal
entities.
Expand the animal package, then expand the details package inside it. Every file you see inside the animal package is a value object. Each data class represents a collection of related attributes, while each enum
represents a closed attribute set. They are simple objects, but a few are worth checking out. You’ll get to them in a few minutes.
For now, you need to add the Animal
entity.
Create a new Animal.kt file in the animal package. In it, add the Animal
class:
data class Animal(
val id: Long,
val name: String,
val type: String,
val media: Media,
val tags: List<String>,
val adoptionStatus: AdoptionStatus,
val publishedAt: LocalDateTime
)
Don’t forget to import LocalDateTime
from the java.time library. Gradle will handle the desugaring for you.
This entity is fairly simple. It has a few primitive properties, a LocalDateTime
for the publishing date and two value objects:
-
media: A
Media
value object instance that handles photos and videos of the animal. -
adoptionStatus: An
enum
value fromAdoptionStatus
.
adoptionStatus
can be one of four values:
enum class AdoptionStatus {
UNKNOWN,
ADOPTABLE,
ADOPTED,
FOUND
}
There’s not much to see here, just a simple enum. Open Media
and take a look at how it’s implemented. You can see that it has two properties:
-
photos: A list of
Photo
objects. -
videos: A list of
Video
objects.
Both Photo
and Video
classes are nested in Media
for ease of access.
Now, take a closer look at Photo
:
data class Photo(
val medium: String,
val full: String
) {
companion object {
const val NO_SIZE_AVAILABLE = ""
}
fun getSmallestAvailablePhoto(): String { // 1
return when {
isValidPhoto(medium) -> medium
isValidPhoto(full) -> full
else -> NO_SIZE_AVAILABLE
}
}
private fun isValidPhoto(photo: String): Boolean { // 2
return photo.isNotEmpty()
}
}
It’s a value object with two properties:
- medium: A link for the medium-sized photo.
- full: A link for the full-sized photo.
There’s also some logic in it:
-
Returns the smallest-sized photo available, which will be useful to display the animal images in the animal list. You don’t need high-resolution images for a list and the smaller the image, the fewer bytes to request from the API.
-
Checks if the photo link is valid. For simplicity, it just checks if the link is not an empty string.
This is good! When you have a piece of logic related to a domain model object, it’s a good practice to keep that logic contained within the object.
Remember the concept of high cohesion? This is a good example of it. The logic has a close relationship with the object, to a point where it ends up using all the object’s properties. This means that you’re not tightly coupling Photo
to something else.
Another important thing to mention in Photo
is its companion object — more specifically, NO_SIZE_AVAILABLE
. This property represents the empty state of a Photo
. It’s a simplified version of the Null Object Pattern, and it’s a nice way to avoid null values.
Yes, you could simply just return an empty string in getSmallestAvailablePhoto()
, like so:
fun getSmallestAvailablePhoto(): String {
return when {
isValidPhoto(medium) -> medium
isValidPhoto(full) -> full
else -> ""
}
}
But that’s not the point. Since it’s such a simple example, NO_SIZE_AVAILABLE
just happens to be an empty string. Don’t look at the values the code is handling; instead, look at its intent. You shouldn’t care about NO_SIZE_AVAILABLE
being an empty string — the important thing here is that NO_SIZE_AVAILABLE
tells you that a Photo
has no sizes available.
Zoom out to Media
and you’ll see that it follows the same approach. It has:
- Highly cohesive logic.
- A simplified Null Object Pattern with
NO_MEDIA
.
Note: For simplicity, this code ignores
Video
. In a more complex example, it would follow the same approach asPhoto
, but with logic for video handling.
AnimalWithDetails Entity
The Animal
entity is enough for this section’s features. However, there will be a details screen later that will need more details than Animal
provides. So you might as well add that functionality now.
In the details package, create a new file, AnimalWithDetails.kt. In it, add AnimalWithDetails
:
data class AnimalWithDetails(
val id: Long,
val name: String,
val type: String,
val details: Details,
val media: Media,
val tags: List<String>,
val adoptionStatus: AdoptionStatus,
val publishedAt: LocalDateTime
)
This entity is exactly the same as Animal
, but it has an extra details
property. You might wonder why you don’t just add a nullable details
property to the Animal
entity. Well, you could. This is just a design choice for the sake of avoiding nullable values. It would be totally OK to go with the nullable property option.
The Details
value object uses the remaining value objects in the packages, along with the Organization
entity.
data class Details(
val description: String,
val age: Age,
val species: String,
val breed: Breed,
val colors: Colors,
val gender: Gender,
val size: Size,
val coat: Coat,
val healthDetails: HealthDetails,
val habitatAdaptation: HabitatAdaptation,
val organization: Organization
)
Nothing’s new here except the Breed
data class. Open it, there’s an interesting detail here that you should be aware of. This is the data class:
data class Breed(val primary: String, val secondary: String) {
val mixed: Boolean
get() = primary.isNotEmpty() && secondary.isNotEmpty()
val unknown: Boolean
get() = primary.isEmpty() && secondary.isEmpty()
}
And this is an example of what the API returns regarding breeds:
"breeds": {
"primary": "Golden Retriever",
"secondary": null,
"mixed": false,
"unknown": false
}
The first obvious change is that secondary
goes from a nullable String
to a non-nullable one. You’ll explore this kind of mapping in the next chapter, so don’t bother with it for now. Apart from this, notice any differences in how information is being passed?
Take a closer look at the properties to understand what they are:
- primary: The primary breed.
- secondary: The secondary breed.
- mixed: Tells you if the animal has mixed breeds — it has both a primary and a secondary breed.
- unknown: Tells you if the animal’s breed is unknown — it has neither a primary nor a secondary breed.
Only the first two properties — primary
and secondary
— are a part of Breed
’s constructor. The other properties, mixed
and unknown
, are deduced from the first two.
Note: Since
Breed
is a data class, it has a few auto-generated methods. Be aware that, in this case, bothmixed
andunknown
are not accounted for by those methods, as they’re outside the constructor.
In all fairness, it’s quite possible that the API also deduces mixed
and unknown
from the other two. It returns them all as independent properties because it has no other option. While it’s true that mixed
and unknown
only add noise, it’s a totally valid way of building a back end: When in doubt, return everything you have. :]
By having some properties depend on others, you increase the cohesion of the class. This increases the amount of information conveyed when you read the class. Also, if you create a copy of the class but change one of the constructor values, both mixed
and unknown
will update. Talk about a good deal!
Adding the PaginatedAnimals Value Object
The previous chapter talks about the API returning chunks of paginated data. The pagination information is also relevant to the UI, letting RecyclerView
request the correct data chunk. It’s a data layer implementation detail, but it ends up leaking to the presentation layer.
So, why not model this information as well? Yes, it doesn’t exactly fit the domain. Still, it’s better to have it modeled and maintain the boundaries between layers than to break the dependency rule even once.
Try not to cut corners on this kind of decision. Otherwise, you’ll start to notice broken windows in your app.
Note: The broken window theory is a theory that describes software decay. It states that visible signs of crime create an environment that fosters more crime. In other words, as soon as you start cutting corners in your app, you’ll do it more and more often.
Expand the pagination package next to the animal and organization packages. Inside, you’ll find there’s already a Pagination
value object. This is the generic representation of the API’s pagination. You’ll now add the specific animal pagination.
Inside the pagination package, create a new file called PaginatedAnimals.kt. Add the following class to the file:
data class PaginatedAnimals(
val animals: List<AnimalWithDetails>,
val pagination: Pagination
)
This value object associates a list of animals with a specific page. It’s exactly what the UI needs to know which page to request next.
You added two entities, a value object, and learned some of the intricacies of domain modeling. Well done! Before diving into repositories, there are still a few domain modeling topics worth addressing.
To Type or Not to Type
Look at Animal
again:
data class Animal(
val id: Long,
val name: String,
val type: String,
val media: Media,
val tags: List<String>,
val adoptionStatus: AdoptionStatus,
val publishedAt: LocalDateTime
)
If you exclude the value objects and the publishedAt
property, you’re left with:
data class Animal(
val id: Long,
val name: String,
val type: String,
val tags: List<String>
)
These properties all have one thing in common: None of them have specific domain types. In fact, they’re just a mix of standard types from the language.
When modeling your domain, you need to make some choices, and those choices have trade-offs. One of the hardest choices to make is how many new domain-specific types you should create.
Types provide safety and robustness in exchange for complexity and development time. For instance, what’s keeping you from creating Animal
with the id
of -1L
? It’s just a Long
type. It doesn’t care about the value you set it to, as long as it’s of type Long
.
However, adding a new type called Id
changes things:
@JvmInline
value class Id(val value: Long) { // 1
init { // 2
validate(value)
}
private fun validate(id: Long) {
if (id.hasInvalidValue()) { // 3
throw InvalidIdException(id)
}
}
}
Here are some things to note in this code:
- Kotlin’s value classes are a neat way to wrap your primitives into custom types, since they (mostly) spare you from the overhead of handling a normal class.
-
init
blocks run immediately after the primary constructor, so this callsvalidate
as soon as you create an instance ofId
. -
hasInvalidValue
is an extension function onLong
that verifies whether the ID value is-1L
or0
. If so,validate
will throw anInvalidIdException
.
Now, imagine that Id
has a specific format. Then, you need to add a new validation:
private fun validate(id: Long) {
if (id.hasInvalidValue()) {
throw InvalidIdException(id)
}
if (id.hasInvalidFormat()) {
throw InvalidIdFormatException(id)
}
}
Suppose that the formatting spec determines the size limit of the ID. It’s a specific case of format validation that deserves its own validation for clarity. By updating the code:
private fun validate(id: Long) {
when {
id.hasInvalidValue() -> throw InvalidIdException(id)
id.hasInvalidFormat() -> throw InvalidIdFormatException(id)
id.exceedsLength() -> throw InvalidIdLengthException(id)
}
}
You also change from a chain of if
conditions to a when
.
It looks clean, but it now throws a bunch of exceptions. You start worrying that it might be hard to maintain the code in the future, especially if you add new validation rule. So, you refactor the whole thing:
@JvmInline
value class Id private constructor(val value: Long) { // 1
companion object {
fun of(id: Long): Either<IdException, Id> { // 2
return when {
id.hasInvalidValue() -> Either.Left(IdException.InvalidIdException(id))
id.hasInvalidFormat() -> Either.Left(IdException.InvalidIdFormatException(id))
id.exceedsLength() -> Either.Left(IdException.InvalidIdLengthException(id))
else -> Either.Right(Id(value = id))
}
}
}
sealed class IdException(message: String): Exception(message) { // 3
data class InvalidIdException(val id: Long): IdException("$id")
data class InvalidIdFormatException(val id: Long): IdException("$id")
data class InvalidIdLengthException(val id: Long): IdException("$id")
}
}
sealed class Either<out A, out B> { // 4
class Left<A>(val value: A): Either<A, Nothing>()
class Right<B>(val value: B): Either<Nothing, B>()
}
Here’s what’s happening, step by step:
- You make the constructor private so that you can fully control new
Id
instance creation. - The old
validate
function is now calledof
, and works as a factory function ofId
’s companion object. With the constructor being private, you’re now forced to doId.of(123L)
if you want to create an instance ofId
. You also change the functions’s signature to be explicit about what’s happening inside. - You encapsulate all the exceptions in the
IdException
sealed class. - You create the
Either
sealed class, a disjoint union to represent success and failure values.
As you can see, it’s pretty easy to get carried away.
Dealing with Boolean
s is also fun. For instance, consider this class:
class User(name: String, email: String, isAdmin: Boolean)
You can see where this is going, can’t you? That isAdmin
is a disaster waiting for the worst moment possible to explode in your face. A simple mistake or a bug that makes the property true
when it should be false
can completely wreck your app.
A common way to avoid stuff like this is to use inheritance:
open class User(name: String, email: String)
class Admin(name: String, email: String) : User(name, email)
Congratulations! You now have one extra class to maintain, and possible inheritance issues that might come from it. You have to agree though, that the code is a lot safer this way.
There’s usually some uncertainty over whether all the extra work will pay off in the future or not. For instance, in the Id
example: Is all of that needed? Maybe some of those cases that you took measures against would never happen anyway. You’d be maintaining all that complexity for nothing!
It’s up to you and your team to decide. Do you want to follow a straightforward, “we’ll refactor when we get there”, YAGNI (You Aren’t Gonna Need It) approach? Or a more time-consuming, type-safe, “model all the things!” way of working?
In general, a solution somewhere in the middle, with just enough design upfront, will fit your needs the best.
Anemic Model
Managing types is not the only common problem in domain models. Sometimes, domain models can become anemic, which means that they mainly consist of data without behavior.
On Android, it’s common for the domain layer to work mainly as a bridge between the other layers. In fact, this is one of the most common arguments against having a domain layer on Android.
PetSave is an example: Other than Breed
, Media
and Photo
, no other domain class has any kind of logic in it. PetSave has what seems like a few anemic models.
However, note that your domain is only starting to take shape. The app is at an early stage, so it’s normal that you don’t have enough domain knowledge to add logic to the models.
It’s possible for the app to grow and its domain to remain anemic. But even so, it’s good to weigh the advantages of having a domain layer on an ever-changing ecosystem like Android before deciding to completely remove it.
This wraps up the domain modeling topics. When you implement use cases later, you’ll need them to access data sources. You’ll have to do it without forcing a dependency on the data layer, to preserve the dependency rule. This is where repositories come in handy.
Inverting Dependencies With Repositories
A repository is a very common pattern for data source abstraction. You use it to abstract away all the data sources you want. Anything that calls the repository can access data from those sources, but will never know which sources even exist.
In the domain layer, you won’t actually implement a repository. Instead, you’ll only have a repository interface. This allows you to invert the dependency on the layers, making the data layer depend on the domain layer, instead of the other way around!
How? it’s simple, and you can start putting it into place right away. In the repositories package, create AnimalRepository.kt. In it, add your interface:
interface AnimalRepository
Later, you’ll implement an actual repository class in the data layer. That class will implement this interface and any of its methods. Then, any time a use case needs data access, you’ll pass it that repository class as a dependency, but the dependency’s type will match the interface’s.
This way, use cases can access all the methods in the interface’s contract, without ever knowing the class that fulfills it. The use case does its job and preserves the dependency rule. Win-win!
For now, you’ll leave the interface just like this. It might be anticlimactic, but it’s much easier to add methods later, when you’re developing the features and know exactly what data you need.
That’s it for the app code for this chapter. For your work as a developer to be complete, though, you’re still one thing missing: tests!
Testing Your Domain Logic
When you build the project, there won’t be any UI changes to let you know that your code works. Still, at least you can rely on tests to tell you that your code does what you expect.
You’ll definitely do some heavy testing later, when you implement use cases. For now, though, there’s not that much to test. Regardless, you want to make sure you start testing as soon as possible.
Your next step is to add tests to verify the domain logic you saw earlier. Adding tests to every class would be redundant for your purposes, so you’ll focus on unit tests for Photo
.
Collapse the petsave package, the root of the project. You’ll see three main packages.
Expand the package that has the (test) label in front of it. This is where you’ll add your tests, since it’s the place where tests that don’t rely on the Android framework should live.
A good way to organize your tests is to mimic the package structure of the app code. This makes it possible for tests to access any internal properties of the code, since anything with the internal visibility modifier is only accessible to code in the same package.
At the root of the package, create the following structure: common/domain/model/animal. Inside animal, create PhotoTests.kt. You’ll end up with something like this:
Test dependencies are already taken care of. Open the file you just created and add the class along with a test:
class PhotoTests {
private val mediumPhoto = "mediumPhoto"
private val fullPhoto = "fullPhoto"
private val invalidPhoto = "" // what’s tested in Photo.isValidPhoto()
@Test
fun photo_getSmallestAvailablePhoto_hasMediumPhoto() {
// Given
val photo = Media.Photo(mediumPhoto, fullPhoto)
val expectedValue = mediumPhoto
// When
val smallestPhoto = photo.getSmallestAvailablePhoto()
// Then
assertEquals(smallestPhoto, expectedValue)
}
}
This test verifies the happy path, with the photo at its smallest available resolution. This Given – When – Then structure is a nice way of organizing your test code. If you maintain these comments, it gets easier to maintain the actual code in the future.
It’s true: The words “maintenance” and “comments” don’t mix. Still, you should keep your tests small and focus on one thing at a time. If you do, it won’t be too hard to keep the comments in place.
Go ahead and add more tests below this one:
@Test
fun photo_getSmallestAvailablePhoto_noMediumPhoto_hasFullPhoto() {
// Given
val photo = Media.Photo(invalidPhoto, fullPhoto)
val expectedValue = fullPhoto
// When
val smallestPhoto = photo.getSmallestAvailablePhoto()
// Then
assertEquals(smallestPhoto, expectedValue)
}
@Test
fun photo_getSmallestAvailablePhoto_noPhotos() {
// Given
val photo = Media.Photo(invalidPhoto, invalidPhoto)
val expectedValue = Media.Photo.NO_SIZE_AVAILABLE
// When
val smallestPhoto = photo.getSmallestAvailablePhoto()
// Then
assertEquals(smallestPhoto, expectedValue)
}
The first test checks if you’re returning the larger photo, in case the medium one is invalid. The second test checks if you’re returning NO_SIZE_AVAILABLE
when both photo sizes are invalid. The only missing test now is for the case when you have a medium photo, but not a full photo. No point in adding it though, as it would be similar to the first test you just added.
These tests are simple. And they should be! They’re unit tests, after all. The important thing here is that they’re actually testing behavior.
Take the last test, for example. You’re initializing Photo
with invalidPhoto
, and the expectedValue
is NO_SIZE_AVAILABLE
. You know that both are empty strings, so why not use the same property everywhere?
As discussed earlier, that’s not the behavior the code wants to achieve. They just happened to both be empty strings — their meanings are vastly different. This is what you have to test: You should test behavior, not code or data.
Key Points
- Domain layers protect you, the developer, and the app’s logic from external changes.
- Entities have an identity that allows you to distinguish between them.
- Value objects enrich your domain and can either contain entities or be contained by them.
- Defining how many custom types to have in your domain is something you should consider carefully. Try to find a balance between under typing and over typing, as both are troublesome.
- Be careful when adding logic to your app that relates to your domain model: That logic might belong inside the actual model classes.
- Repository interfaces allow for dependency inversion, which is essential to keep the domain layer isolated.
- Test behavior, not code or data.
You’ve reached the end of the chapter. Awesome! Next, you’ll learn about the data layer.