What I Learned at Work this Week: Layered Architecture

I’m currently reading Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans for my company’s book club. Loyal readers of this blog will know that I generally struggle with textbooks like this. I start reading, committed to focusing and learning a concept, but once it starts getting complicated I end up confused. Since I can’t afford to devote the time needed to go back and try to understand it, I accept the amount I did learn and move on. But this becomes a problem because future chapters rely on a certain level of knowledge that I have not achieved.

In Domain-Driven Design, Layered Architecture is one of those core concepts that comes back over and over again. I’m never going to be able to read something in a textbook and understand it the first time, but I believe it’s worth taking extra time to struggle with this concept because it will help enrich my experience with this book and help make me a better developer. So what is Layered Architecture?

For those (like me) who find a video much easier to follow than a book

As I sat down to write, I realized that not only did I need help understanding Layered Architecture, but that I struggled to even explain the core concept of the book: Domain-Driven Design. Before we break down the above screenshot, let’s talk about the term “domain.” A domain is the universe in which our software lives. Usually we can identify the domain by describing the purpose of our software: accounting software lives in the accounting domain, educational software in education, and genetic testing software in…genetic testing. All three of these domains might feature software that has similar underlying logic when we dig deep down, but Domain-Driven Design will encourage us to understand how non-technical parts of these fields fit together.

Looking at the makeup of Domain-Driven Design helped me understand the definition of a domain. As we saw earlier, Eric Evans breaks DDD into four pieces:

  1. Focus on the core complexity and opportunity of the domain: As engineers, our first instinct may be to ask how we are going to write logic to execute the task at hand. If we were writing accounting software, for example, how are we going to take hundreds of inputs (line items) and turn them into one comprehensive result (a final expense report). Domain-Driven Design means we’re going to first understand how the domain works conceptually — to learn how accountants think about their own world — and use that knowledge to drive the development of our own models for software.
  2. Explore models in a collaboration of domain experts and software experts: the best way to learn how an accountant thinks is…to ask one! Evans stresses that this must be a collaborative process; we engineers shouldn’t just build the models ourselves with no input from experts, but we also shouldn’t just hand the task off to those experts and expect workable models to come back. We have to work together to assure that we’ll create something that’s both functional and understandable for both sides.
  3. Write software that expresses those models explicitly: When we see the word model, we can think of a picture that connects concepts within a domain. Returning to the accounting domain, we might say that an expense report is made up of individual line items which should be totaled up into gross expenses (this is surely not accurate, hence the need for domain experts). When Evans says that our software must express the models explicitly, he means that we should create an ExpenseReport class that accepts a lineItems collection during construction and has an addLineItem() method that we can invoke to update the list. We want to be speaking the domain’s language as deep down into the code as we can.
  4. Speak a ubiquitous language within a bounded context: Ubiquitous language is the term the book uses to describe domain-specific references like “credit,” “debit,” or “expense report.” We should work as a team with domain experts to speak in a language that all parties can understand. Evans stresses a bounded context because the ubiquitous language must exist within the context that we develop or else it will stop making sense. “Credit” won’t mean the same thing in our accounting model as it would in an education model because each one lives in its own bounded context.

From the text:

In an object-oriented program, UI, database, and other support code often gets written directly into the business objects. Additional business logic is embedded in the behavior of UI widgets and database scripts. This happens because it is the easiest way to make things work, in the short run.

When the domain-related code is diffused through such a large amount of other code, it becomes extremely difficult to see and to reason about.

Evans is describing a failure to adhere to the principle of separation of concerns. He’s saying that business objects (expense reports and line items in our example) should only be responsible for logic that changes their immediate surroundings. If our ExpenseReport class has an addLineItem method, the logic in that method should change the lineItems property of our ExpenseReport instance, but it should not also directly contain logic that updates the UI or even adds the new lineItem to the database. To properly separate our concerns and make our software easier to understand and iterate on, we must place that logic in a different layer of the architecture. Evans says that most successful architectures use some version of these four conceptual layers:

  1. User Interface (or Presentation Layer): This is the messaging layer. It accepts commands from users and displays the results of their inputs.
  2. Application Layer: This layer contains the logic behind the User Interface, determining what to do when an input is received. Evans stresses that it should be kept thin, primarily relaying commands to the next layer (the Domain Layer). The Application Layer doesn’t deal with business logic, meaning it’s not responsible for rules that determine whether an input will fail or succeed (like if a user adds a lineItem that puts us over budget) — it just relays commands and responses.
  3. Domain Layer (or Model Layer): Here is where we find our business logic and the heart of our application. Think of the Domain Layer as the software equivalent of a domain expert — when a user inputs a command (“please submit my expense report for this week”), the Domain Layer will be where we find out whether this works or not. It may determine that the submission is fine, or there may be business logic that says “all expense reports must be signed before submission. This report has no signature and therefore fails my verification check.” The Domain Layer doesn’t know what words to use to send the message or how it should be displayed, but it sends a failure back up the ladder to the Application Layer, which forms a message and sends it to the User Interface, which renders it.
  4. Infrastructure Layer: This final layer handles the technical, non-business related requirements of our software. It persists data or parses lists, sends emails or renders images. These are all functions that may be needed by any type of program, so they don’t belong in the Domain Layer, but have to end up somewhere.

What I’ve covered here is less than one chapter of Eric Evans’ book, but breaking it down has unlocked a lot of understanding for me. I’m hoping that the next time I read the words “this will cause an issue because the logic belongs in the Domain Layer,” I won’t feel completely lost. And, beyond that, I hope that I’ll be able to apply this knowledge and everything I learn about Domain-Driven Design someday as I actually contribute to the architecture of a system. As always, we’ll get there little by little.

Solutions Engineer