Skip to content

Architecture

Overview

Before we start writing code, let's talk about how we should organize our application.

Over the years there have been many best practices for organizing code. These practices evolve as technologies change and we learn from maintaining previous approaches.

When we look at the application as a whole and consider all of its high-level parts, we're talking about the architecture of the application. The architecture describes the general division of responsibilities in your application, and which parts communicate with which other parts.

Breaking your application into chunks allows for

  • Easier discussion of how the application works
  • Easier developer tasking (especially in larger applications)
  • Reuse of chunks across different types of applications (reusing the data layer, for example, across mobile apps, web services, and command-line applications)
  • Easier independent testing of separate parts of your application

The architecture we use in this class is based on Modern Android Development, also known as "MAD". See https://developer.android.com/modern-android-development, which includes architecture, tools and libraries that Google recommends for Android development.

Architecture Overview

Applications are structured as Layers and Modules.

A "module" is a separately-compilable group of code and resources that can be consumed by other modules in your system. They're useful for isolating functionality, internal implementation details can be hidden while exposing a subset of function, its public API (Application Programming Interface). Separating code into modules also helps large applications isolate changes that many developers are making at the same time.

A "layer" is a grouping of one or more modules with certain responsibilities. Layers allow you to explicitly define communication between modules and further improve ease of maintenance.

Application Layers

graph TD
    UI[User-Interface Layer] -->|depends on| Domain
    Domain[Domain Layer] -->|depends on| Data[Data Layer]

At a high level, we talk about application layers, separating data from its manipulation and presentation.

Each layer isolates a responsibility in the application. At its simplest, each layer could be exactly one module. (While layers could be just conceptual grouping of code/resources that are all in a single module, using at least one module per layer gives us the ability to restrict what's exposed between layers.)

Layers could contain multiple modules, which I recommend as the application grows in size.

The Data Layer manages data acquisition and update. You can use it to persist data locally in a database, file or in memory. Or maybe your application connects to a server for its data; you can use the Data Layer to connect to a server to grab data and push changes.

By keeping your data access code inside the Data Layer, you can abstract and hide the how data is accessed from the rest of the application. If your data management code changes over time, the rest of the application may require fewer changes to adapt.

Operations in the Data Layer are sometimes called Primitive Operations, as they're the core, often simplest, operations you define on your data. Many primitive operations are simply property changes to objects and CRUD operations (Create/Read/Update/Delete) against your data store. You may also define more complex operations that are always needed for any user of the data.

The Data Layer doesn't depend upon other architectural layers, but may depend upon other modules/libraries such as database storage or network communication.

The Domain Layer is an optional layer that increases in value as the size of your application grows. Often you'll find that several parts of your application need to perform the same sequences of primitive operations. For example, a game might have several places that put an item in the player's inventory. It would be a great idea to create a function to contain the repeated code, but where do we put that function?

In a smaller application, you could add a function in the User-Interface Layer, and you've successfully factored out that common functionality.

As the application grows, the user interface might be broken down into multiple modules. Which UI module should host that functionality?

Or if you want to use the same "put item in inventory" for an Android application, desktop application, and web application, you no longer have a common user interface to host it.

A Domain Layer hosts this type of common functionality and gives you access from any user interface components, within the same application or across multiple applications.

These types of operations are generally called "macro operations". They build upon primitive operations by combining primitives to create more complex processing of the data.

In recent architectures, these types of operations are called Use Cases or Interactors.

The Domain Layer only depends upon the architectural Data Layer for access to its data. It can also depend upon other modules/libraries that provide frameworks, algorithms or support functions to work with the data.

Finally, we use our User-Interface Layer to present data to the user and interpret their actions. This layer can present the data using many different approaches: graphically, textually, as a service, or using assistive technologies for example.

Note

I've called out "as a service" as a type of User-Interface implementation. Think about what it means to be a "user". The "outsider" that's interacting with your application is its user. That could be a person, or it could be other applications. When we think of a service (such as a web service) as a "user interface", we're talking about a layer that prepares data for presentation (creating a JSON output, for example) and interprets user interaction (receives the service call, figures out what it means, and calls the appropriate functionality in the Domain (if present) or Data layers.)

The User-Interface Layer only depends on the Domain Layer (if present) or the Data Layer. This is somewhat controversial; some want to allow the User-Interface Layer to use the Domain Layer for complex operations, and skip past it directly access the Data Layer for simpler operations. The problem here is that it's possible to miss added value in the Domain Layer with this approach.

Suppose you have a Domain Layer that exposes macro operations to the User-Interface Layer, and also passes-through primitive operations from the Data Layer. If we allow the User-Interface Layer a choice of which layer it wants to work with, it may miss changes that the Domain Layer makes. For example, later on, as part of the simpler pass-throughs, if the Domain Layer adds some logging and verification (that the Data Layer doesn't have), it will be missed if the User-Interface Layer skipped around the Domain Layer to directly communicate with the Data Layer.

When using a Domain Layer, all User-Interface Layer modules must only depend on modules in the Domain Layer.