Low-level designDeep dive10 min

Layered architecture

Split an application into layers, such as presentation, domain logic and data access, and let each layer depend only on the ones below it. A lower layer returns answers instead of calling up, so any number of front ends can sit on top of the same rules.

Watch it evolve1 / 10
The problem

A screen, the rules, and a store

OrderScreen takes the order, OrderService checks the bakery's rules, and OrderStore keeps and numbers the orders.

80
Start
Stage 1
Stage 2
Final

1 · The situation

A bakery's preorders, in three classes

A bakery takes cake preorders on a screen at the counter. The code is already split three ways, along the usual lines:

  • OrderScreen takes what the customer typed and shows the answer;
  • OrderService holds the bakery's rules, such as no pickups on Monday, when the shop is closed;
  • OrderStore keeps the orders and numbers them.

To show the confirmation as soon as an order is stored, the first version lets OrderStore call a static helper on the screen, OrderScreen.showSaved(), right after saving. Python writes it show_saved(). It's one line, and it works.

The first versionJava
import java.util.ArrayList;
import java.util.List;

record Order(int id, String customer, String cake, String day) {
    String summary() {
        return cake + " for " + customer + " on " + day;
    }
}

class OrderScreen {
    private final OrderService service;

    OrderScreen(OrderService service) {
        this.service = service;
    }

    void submit(String customer, String cake, String day) {
        try {
            service.place(customer, cake, day);
        } catch (IllegalArgumentException e) {
            System.out.println("Screen: " + e.getMessage());
        }
    }

    static void showSaved(Order order) {
        System.out.println("Screen: order #" + order.id() + " saved, " + order.summary());
    }
}

class OrderService {
    private final OrderStore store;

    OrderService(OrderStore store) {
        this.store = store;
    }

    void place(String customer, String cake, String day) {
        if (day.equals("Monday")) throw new IllegalArgumentException("closed on Monday");
        store.save(customer, cake, day);
    }
}

class OrderStore {
    private final List<Order> orders = new ArrayList<>();

    void save(String customer, String cake, String day) {
        Order order = new Order(orders.size() + 1, customer, cake, day);
        orders.add(order);
        OrderScreen.showSaved(order);
    }
}

public class Main {
    public static void main(String[] args) {
        OrderScreen screen = new OrderScreen(new OrderService(new OrderStore()));
        screen.submit("Ana", "lemon cake", "Friday");
        screen.submit("Cy", "plum tart", "Monday");
    }
}
*«record»Order− id: int− customer: String− cake: String− day: String~ summary(): StringOrderScreen− service: OrderService~ submit(String, String, String)~ showSaved(Order)OrderService− store: OrderStore~ place(String, String, String)OrderStore− orders: List<Order>~ save(String, String, String)

The class diagram of this version, drawn from its code: the screen calls the service, the service calls the store, and the store calls the screen.

Ana orders a lemon cake for Friday, and Cy tries to pick up a tart on a Monday:

Screen: order #1 saved, lemon cake for Ana on Friday
Screen: closed on Monday

2 · The change

A phone line, and a confirmation shown on the wrong screen

The bakery starts taking orders by phone. A second front end, PhoneOrders, uses the same OrderService, so the rules apply to phone orders too. The new class is short:

The phone front endJava
class PhoneOrders {
    private final OrderService service;

    PhoneOrders(OrderService service) {
        this.service = service;
    }

    void call(String customer, String cake, String day) {
        try {
            service.place(customer, cake, day);
        } catch (IllegalArgumentException e) {
            System.out.println("Phone: " + e.getMessage());
        }
    }
}

Ben phones in an order for a carrot cake. Look at where his confirmation goes:

Screen: order #1 saved, lemon cake for Ana on Friday
Screen: closed on Monday
Screen: order #2 saved, carrot cake for Ben on Saturday

Ben's order is saved, and its number appears on the counter screen. The phone line has nothing to tell him.

The store doesn't know who asked. It always reports to the counter screen, because that's what it calls. PhoneOrders never learns the order number, so the person on the phone can't tell Ben what it is.

The answer travels up to the one front end the store knows about, not back to the one that asked.

The call back up costs more than this bug:

  • The three classes form a loop. The screen needs the service, the service needs the store, and the store needs the screen. None of them can be used, built or tested without the other two.
  • Every new front end hits it. A web form or a delivery app would have the same problem, and each fix would add another call from the store to some screen.
  • The data layer knows about the screen. Changing how a confirmation looks means editing the class that stores orders.

In C++ the loop even shows in the file: the classes can't each be defined in full before the next, so OrderScreen::submit() is declared inside the class and defined after all three. The practice checks flag it: OrderScreen, OrderService and OrderStore depend on each other.

3 · The fix

Point every dependency down

Start from the phone version. The rule to apply: a class may call the layer below it, and the layer below answers by returning a value, never by calling up.

OrderStore.save() returns the new Order instead of calling the screen. The store no longer knows any screen exists.

Java
class OrderStore {
    private final List<Order> orders = new ArrayList<>();

    Order save(String customer, String cake, String day) {
        Order order = new Order(orders.size() + 1, customer, cake, day);
        orders.add(order);
        return order;
    }
}
*«record»Order− id: int− customer: String− cake: String− day: String~ summary(): StringOrderScreen− service: OrderService~ submit(String, String, String)PhoneOrders− service: OrderService~ call(String, String, String)OrderService− store: OrderStore~ place(String, String, String): OrderOrderStore− orders: List<Order>~ save(String, String, String): Order

The counter screen shows the same lines as before, and Ben hears his order number:

Screen: order #1 saved, lemon cake for Ana on Friday
Screen: closed on Monday
Phone: your order number is 2
Before: classes in the loop
3
After: classes in a loop
0

Classes that depend on each other in a circle. After the fix, the store and the service can be used with no screen at all.

4 · The idea

Layers: presentation, domain logic, data access

A layered architecture groups an application's code by the kind of work it does and stacks the groups. The usual three are:

  • Presentation: what the user sees and types, such as screens, pages and command lines;
  • Domain logic, also called business logic: the rules of the business, here the bakery's;
  • Data access: storing and finding data, in memory, files or a database.

Dependencies run from top to bottom: presentation calls the domain, and the domain calls data access.

*«record»Order− id: int− customer: String− cake: String− day: String~ summary(): StringOrderScreen− service: OrderService~ submit(String, String, String)PhoneOrders− service: OrderService~ call(String, String, String)OrderService− store: OrderStore~ place(String, String, String): OrderOrderStore− orders: List<Order>~ save(String, String, String): Order

The layered version: two front ends on top, the rules in the middle, the store at the bottom, and every arrow pointing down.

Layer

Here

May depend on

Never depends on

Presentation

OrderScreen, PhoneOrders

Domain logic

No layer: it sits on top

Domain logic

OrderService

Data access

Presentation

Data access

OrderStore

The database or files

Domain logic or presentation

Two variants differ in how far a layer may reach:

  • In strict layering, a layer calls only the layer directly below it, so the screen never reads the store directly.
  • In relaxed layering, a layer may call any layer below it. A screen that lists today's orders straight from the store skips the rules, which is quicker to write and adds one more dependency to keep in mind.

Layers are about how the code is organized. Tiers are about where it runs: the same three layers can run in one process, as here, or on a browser, an app server and a database server.

5 · When breaking it is fine

Pass-through code, and layers that grow too big

Layering has costs:

  • Pass-through methods. When a request needs no rule, the middle layer only forwards the call, as OrderService.place() mostly does here. A few are fine; a service made only of forwards is a layer with nothing to do.
  • One feature, three places. Adding a field to an order touches the screen, the service and the store.
  • Layers aren't modules. In a large application each layer gets big, and a change to one feature crosses all of them. Big systems usually split by feature first, such as orders, payments and deliveries, and layer each of those inside.

Situation

What to do

A short script that reads a file and prints a total

One class or function; layers add nothing

A screen that only lists records, with no rules

Relaxed layering: let it read the data layer directly

A lower layer must report progress or events upward

Let it call an interface it owns, such as a listener, which the upper layer implements

A large application with many features

Split by feature first, then layer inside each feature

6 · Getting it right

Answers go up as return values

  • Return, don't call up. A lower layer returns a value or throws. That's what let one service serve two front ends here.
  • When a lower layer must notify, invert the dependency. Give the lower layer an interface of its own, such as OrderListener, and let the upper layer implement it. The arrow then points down again, from the screen to the interface.
  • Keep the rules out of the screen. If OrderScreen checked for Mondays, the phone line would skip the check.

Two more habits keep each layer to its job:

  • Keep storage details out of the rules. The service asks the store to save; it never builds a file name or a query.
  • Test each layer from below. The service can be tested with a store and no screen; the store with nothing above it.

Each language can make the layers visible in its own way:

Language

A layer is usually

Keeping the direction

Java

A package, such as bakery.web, bakery.orders, bakery.data

Package-private classes, Java modules, or an architecture test that fails on an upward import

C++

A directory and a library target in the build

Each library links only to the ones below it

JavaScript

A folder of modules

A lint rule that forbids imports from a higher folder

Python

A package

An import-linting tool configured with the layer order

7 · Look-alikes

Layers, tiers and the architectures built on them

Several ideas divide an application. What tells them apart is what they divide by.

Idea

What it divides, and how

Layered architecture

Code by kind of work, stacked, with dependencies pointing down

Tiers

Where the code runs: browser, app server, database server

MVC and its relatives

The presentation layer itself, into model, view and controller

Ports and adapters

Puts the domain at the center; storage and screens both depend on it, so even data access points inward

Packages and dependency rules

Turns the layer order into rules a build can check

Ports and adapters is the main alternative. In plain layering the domain depends on data access. In ports and adapters the domain owns an interface for storage, and the data access code implements it, so the domain depends on nothing outside itself.

8 · In practice

Where you'll meet the three layers

Many frameworks name the layers for you. Spring, a Java framework, has stereotype annotations for them: @Controller for the presentation layer, @Service for the service layer and @Repository for the persistence layer. A class marked @Repository that imports a controller is the same upward call as OrderStore calling OrderScreen.

In the practice problems, the checks look for loops between classes in any layer: classes that depend on each other in a circle are reported as "A, B and C depend on each other", with the advice to let one side depend on an interface or to move the shared part out.

Check yourself

4 questions

1. The bakery adds a delivery app as a third front end. In the layered version, which classes change?
2. The store must report each saved order to a kitchen display as it happens. What keeps the layering intact?
3. A screen lists today's orders by reading OrderStore directly, skipping OrderService. What is that?
4. What's the difference between a layer and a tier?

Takeaways

Remember this

  • Group code into presentation, domain logic and data access, and let each layer depend only on the ones below it.
  • A lower layer answers by returning values; a call back up ties it to one caller and makes a loop.
  • When a lower layer must notify, it calls an interface it owns, and the upper layer implements it.
  • Strict layering calls only the next layer down; relaxed layering may skip, at the cost of more dependencies.
  • Layers organize code; tiers say where it runs. Large systems split by feature first and layer inside.