Untangling the Business Logic: An Android Story

Domain Model vs Clean Architecture Use Cases

Dec 17, 2019 | Emmanuel Lagarrigue Lazarte

I started developing for the Android platform over ten years ago. At that time, I was a junior developer with little knowledge of software architecture, and my typical project structure was just a list of Activities (with a couple thousand lines of Java code each). We all know the result of that is a fragile code base, which is really difficult to maintain, and nearly impossible to test.
After some years of professional growth (together with community maturity), I started adopting architectural patterns to my Android projects. My initial approach was a basic three-layer architecture as follows:



The Presentation layer evolved by itself, from heavy big Activities to patterns like MVC, MVP, MVVM, or MVI.

The Domain Layer holds the business logic. Its design followed the Domain Model pattern described by Fowler (“Patterns of Enterprise Application Architecture”, 2003). But Android projects were always different from Enterprise Applications, as they were never centered around data, but driven by application events and user interface.

The Domain layer evolution corresponds to the general architecture evolution: Uncle Bob’s “Clean Architecture” (2017):



Why do I say evolution? The Domain layer in clean architecture separates Entities from Use Cases, yielding a more readable, testable, and flexible solution. We will discuss this evolution in more detail in the following sections.

The Revenue Recognition Problem

In his book “Patterns of Enterprise Application Architecture”, Martin Fowler uses the following example to explain the different domain logic patterns:

"Revenue recognition is a common problem in business systems. It’s all about when you can actually count the money you receive on your books.
... we’ll imagine a company that sells three kinds of products: word processors, databases, and spreadsheets. According to the rules, when you sign a contract for a word processor you can book all the revenue right away. If it’s a spreadsheet, you can book one-third today, one-third in sixty days, and one-third in ninety days. If it’s a database, you can book one-third today, one-third in thirty days, and one third in sixty days."



You can see a transcription of the implementation here (Kotlin).

What’s wrong with this approach?

In terms of correctness, actually, there’s nothing wrong with it. Moreover, this also holds for some modifiability aspects such as adding new recognition strategies, since it complies with the Open/Closed SOLID principle. So, in my opinion, the main problem is Readability. The logic can be hard to follow and it is a problem to find stuff. This is reinforced by what Fowler says in his book:

“A common thing you find in domain models is how multiple classes interact to do even the simplest tasks. This is what often leads to the complaint that with OO programs you spend a lot of time hunting around from class to class trying to find them. There’s a lot of merit to this complaint.”

And that’s a critical issue because it affects development as maintenance. It’s not a problem for the future, it’s time we are wasting now.

Another problem with the domain model is that flexibility usually comes at a high cost. In this example, the RecognitionStrategy is the cost of being flexible. Design patterns are awesome, but the resulting design has a higher complexity (shut up and take my money).

Finally, it’s hard to end up with low coupling in our design (unless we apply design patterns for every possible and impossible future change), and that affects modifiability. This becomes evident with the Contract class in the example. What if we need to get recognition strategies from an external service? How would that affect every class that uses Contract? Also, a higher coupling complicates testability.


The Clean Architecture Domain approach

The key in Clean Architecture Domain is to think about use cases and entities. A very important thing to bare in mind is that entities are use case agnostic. And that’s what resolves the issues described above.



Here’s the implementation.

Check the tests. They are identical in functionality, for example:

Domain Model
    {
        val signDate = DateTime.parse("2019-3-15")
        val contract = ContractFactory.get(
            Product.newSpreadsheet("Thinking Calc"),
            Money.of(CurrencyUnit.USD, 999.0),
            signDate
        )

        val revenueBefore = contract.recognizedRevenue(signDate.minusDays(10))
        val revenueAfter2Weeks = contract.recognizedRevenue(signDate.plusDays(2))
        val revenueAfter2Months = contract.recognizedRevenue(signDate.plusDays(62))
        val revenueAfter4Months = contract.recognizedRevenue(signDate.plusDays(92))

        revenueBefore shouldEqual Money.of(CurrencyUnit.USD, 0.0)
        revenueAfter2Weeks shouldEqual Money.of(CurrencyUnit.USD, 333.0)
        revenueAfter2Months shouldEqual Money.of(CurrencyUnit.USD, 666.0)
        revenueAfter4Months shouldEqual Money.of(CurrencyUnit.USD, 999.0)
    }
Clean Architecture Domain
    {
        val signDate = DateTime.parse("2019-3-15")
        val contract = CreateContractUseCase.execute(
            Product.Spreadsheet("Thinking Calc"),
            Money.of(CurrencyUnit.USD, 999.0),
            signDate
        )

        val revenueBefore = 
              GetRecognizedRevenueUseCase.execute(contract, signDate.minusDays(10))
        val revenueAfter2Weeks =
              GetRecognizedRevenueUseCase.execute(contract, signDate.plusDays(2))
        val revenueAfter2Months = 
              GetRecognizedRevenueUseCase.execute(contract, signDate.plusDays(62))
        val revenueAfter4Months = 
              GetRecognizedRevenueUseCase.execute(contract, signDate.plusDays(92))

        revenueBefore shouldEqual Money.of(CurrencyUnit.USD, 0.0)
        revenueAfter2Weeks shouldEqual Money.of(CurrencyUnit.USD, 333.0)
        revenueAfter2Months shouldEqual Money.of(CurrencyUnit.USD, 666.0)
        revenueAfter4Months shouldEqual Money.of(CurrencyUnit.USD, 999.0)
    }

The difference is that with Domain Model we get the contract object from the factory, and we call contract.recognizedRevenue(date). With a clean domain, we need to execute two use cases: CreateContractUseCase and GetRecognizedRevenueUseCase.


Conclusion

I believe Clean Architecture Use Cases solution improves the domain model approach with regards to the following aspects:

  • Readability: It's so much faster to find stuff! Use cases tend to be small (Single Responsibility Principle), and that makes it much easier to read and understand a class.
  • Modifiability: We decouple business logic from the entities into use cases. Entities should be hardly changed, whereas use cases can change a lot. Having those two isolated makes our life easier. In the previous example, new recognition strategies requirements would affect the way in which the use case is called, but it would not affect any class that depends on Contract.
  • Testability: One of the goals of Clean Architecture is making tests simpler. Use cases (and general dependencies) are easy to mock thanks to the general decoupling.

Of course, there are downsides. This design is not free, and we usually end up with a lot of classes (shut up and take the rest of my money). But once the architecture is set up, you will see how productivity goes up.


Thanks to all the reviewers and Hannah Gambino for the illustration.

Be sure to follow @doximity_tech if you'd like to be notified about new blog posts.