Grokking Modern Software Architecture Styles
Vote

0% completed

​

Layered (N-Layer) Architecture

Know the Vocabulary

Part 1: What Is Layered Architecture

The Core Idea

Where Layering Comes From

Part 2: The Three Layers and Their Responsibilities

Layer 1: The Presentation Layer

Layer 2: The Business Logic Layer

Layer 3: The Data Access Layer

Part 3: The Four-Layer Variant

Part 4: Dependency Injection and Wiring

Part 5: Testing Each Layer Independently

Testing the Business Logic Layer

Testing the Presentation Layer

Part 6: The Strict Dependency Rule and Its Violations

The Rule

Closed Layers, Open Layers, and the Sinkhole

Common Violations and Their Costs

Enforcing the Rule with Tools

Part 7: Advantages and Limitations

Real Advantages

Real Limitations

Part 8: When Layered Architecture Is the Right Choice

Use It When

Consider Alternatives When

FAQs

Key Takeaways

Know the Vocabulary

This lesson uses the terms below repeatedly. Read them once now and return here when a later section needs one.

  • Layer: A horizontal slice of the code with one technical job, such as handling HTTP or talking to the database.
  • Separation of concerns: Keeping each kind of work, such as request handling, business rules and storage, in its own part of the code.
  • Presentation layer: The layer that receives external input, such as an HTTP request, passes it to the business layer, and formats the response.
  • Business logic layer: The layer that holds the domain rules and the use cases, with no knowledge of HTTP or of the database.
  • Data access layer: The layer that reads and writes persistent storage and translates database records into domain objects.
  • Controller: A function or class in the presentation layer that handles one kind of request, such as creating an order.
  • Domain object: A class that models one business concept, such as an order, and holds both its data and the rules about its own state.
  • Repository: The class that reads and writes one kind of database record on behalf of the business logic.
  • Interface: A declaration of the methods a class must provide, with no implementation. In Python, an abstract base class plays this role.
  • Dependency inversion principle: A rule where the higher-level module owns the abstraction and the lower-level module implements it. Here, the business layer defines the repository interface and the data access layer implements it.
  • Transaction: A group of database writes that either all succeed or all roll back together (atomicity).
  • Integration layer: An optional fourth layer that talks to external systems, such as payment processors and email providers, through interfaces the business layer defines.
  • Dependency injection: Giving a class the objects it depends on, usually through its constructor, instead of letting the class create them.
  • Composition root: The one place where the application creates its objects and connects them, usually at or near its entry point (a function in a startup file).
  • Test double: A stand-in object that replaces a real dependency in a test, such as a Mock object in place of a repository.
  • Anemic domain model: An anti-pattern in which domain objects hold only data and all of the logic lives in service classes.

Part 1: What Is Layered Architecture

The Core Idea

Layered architecture organizes code into horizontal layers where each layer has a specific, exclusive responsibility. Each layer provides services to the layer above it and consumes services from the layer below it. The strict rule: a layer may only communicate with the layer directly adjacent to it.

The N in N-Layer is the number of layers. Three is the classic count, and Part 3 adds a fourth layer for external integrations. A layer is a logical grouping of code, so the count says nothing about deployment. All of the layers in this lesson usually run in one process, and the question of how code is spread across separate machines (tiers) belongs to the N-Tier Architecture lesson.

The classic three-layer model:

Layered Architecture
Layered Architecture

Each layer hides its implementation details from the layers above it. The presentation layer does not know whether data comes from PostgreSQL, MongoDB, or a flat file. The business logic layer does not know whether the request came from an HTTP endpoint, a CLI command, or a background job. The data access layer does not know what business rules apply to the data it retrieves.

This separation is not about organizing code into folders. It is about limiting what each layer knows. Each layer knows as little as possible about the layers around it. That limit is what lets each layer be tested and understood on its own, and it keeps most changes, including the replacement of a database, inside a single layer.

Where Layering Comes From

Layering is one of the oldest ideas in software design. Edsger Dijkstra built the THE operating system as a stack of levels, where each level used only the services of the levels below it. Network protocol stacks such as the OSI model follow the same idea, and the book Pattern-Oriented Software Architecture describes it as the Layers pattern.

Many early web applications ignored this idea and mixed all concerns in the same files. A single script might query the database, apply business rules, format HTML, and handle user input, all in one 500-line file.

The problems this created were consistent and predictable:

Testing was nearly impossible. To test whether a business rule was correct, you needed a running database, a web server, and the ability to simulate an HTTP request. There was no way to test the rule in isolation.

Changes had unpredictable consequences. Changing how a product's price was calculated might require finding and updating that logic in six different files where it had been copied. Missing one meant a bug.

Replacement was expensive. Switching from one database to another required searching for database code throughout the entire application rather than modifying one isolated layer.

Onboarding new engineers was slow. Understanding any feature required reading and understanding all three concerns simultaneously. There was no clear place to look for business rules, presentation logic, or data access code.

Layered architecture applies the old idea of layering to application code. It gives each concern its own layer, and that separation of concerns prevents these problems.

Unstructured Code vs. Layered Architecture
Unstructured Code vs. Layered Architecture

Part 2: The Three Layers and Their Responsibilities

Layer 1: The Presentation Layer

The presentation layer is the entry point for all external interaction. It is responsible for receiving input, translating it into a format the business logic layer understands, calling the business logic layer, and formatting the response.

What it owns:

  • HTTP request parsing and routing
  • Input deserialization (JSON to Python objects, XML to Java objects)
  • Request validation (are required fields present? are values the correct type?)
  • Authentication (is this request from an authenticated user?)
  • Response serialization (domain objects to JSON)
  • HTTP status code selection
  • Error translation (business exceptions to HTTP error responses)

What it is forbidden from doing:

  • Applying any business rules
  • Directly querying the database
  • Making decisions based on business domain logic
  • Knowing anything about how data is stored

The presentation layer is thin. Its job is to translate between the external protocol (HTTP, gRPC, CLI arguments) and the internal language of the application (method calls with domain objects). It delegates all meaningful work to the business logic layer.

The code below is the Flask controller for creating an order. It receives the order service that startup code built, so the controller never constructs the service itself. The status is sent as its plain lowercase string value, such as "confirmed".

Python3
Python3
. . . .

Notice what is not in this controller: no database queries, no inventory checking logic, no price calculation, no payment processing. The controller knows HTTP. It delegates everything else.

Presentation Layer: Handling HTTP Requests
Presentation Layer: Handling HTTP Requests

Layer 2: The Business Logic Layer

The business logic layer is the most important layer of the application. It contains all domain rules, all use cases, all validation that relates to the business domain (not just request format), and all orchestration of the operations that fulfill a user request.

What it owns:

  • Domain rules (a user cannot order more than 10 of a limited item)

  • Use case orchestration (to create an order: check inventory, calculate price, charge payment, create order record)

  • Business validation (an order requires at least one item, quantities must be positive)

  • Domain calculations (tax calculation, discount application, shipping cost estimation)

  • Domain events (an order was created, a payment was processed)

  • Unit of work boundaries (deciding which writes must succeed or fail together in one transaction)

What it is forbidden from doing:

  • Knowing anything about HTTP, gRPC, or any other transport protocol
  • Directly importing or calling database libraries (SQLAlchemy, psycopg2, Django ORM)
  • Knowing where data comes from (database, cache, external API)
  • Knowing how responses are formatted for the client

In classic layering, the business logic layer imports the concrete repository classes of the data access layer and calls them directly. Both the call direction and the source-code dependency point downward. OrderService imports PostgresOrderRepository, so a change to that class can force a change in the service, and a unit test of the service must supply that concrete class or patch it.

The design in this lesson takes one step beyond classic layering. The business layer declares the repository operations it needs in an interface that it owns, and the data access layer implements that interface. The calls still run downward, but the source-code dependency now points upward, from the data access layer to the interface in the business layer. A rule where the higher-level module owns the abstraction and the lower-level module implements it is the dependency inversion principle. The same step, applied at every boundary, is what the hexagonal style does, and it is what lets the business layer be tested with a fake repository and no database.

The code below shows the two repository interfaces the business layer owns. An abstract base class (a class whose methods have no body and must be supplied by a subclass) is all the business layer needs to describe them.

Python3
Python3
. . . .

The code below defines the order domain objects. The tax rule and the cancellation rule live on the Order class itself, so they need no database to run.

Python3
Python3
. . . .

The code below is the create order use case. Notice that products are matched to quantities by id, because a database query with an IN clause returns rows in no fixed order.

Python3
Python3
. . . .

The code below adds the cancel use case to the same service. The service checks ownership and then lets the Order object decide whether its status allows cancellation.

Python3
Python3
. . . .
Business Logic Layer: Processing an Order
Business Logic Layer: Processing an Order

Layer 3: The Data Access Layer

The data access layer handles all interaction with persistent storage. It translates between the domain objects the business layer uses and the storage format the database uses.

What it owns:

  • Database queries (SELECT, INSERT, UPDATE, DELETE)
  • ORM model definitions and mappings (an ORM, or object-relational mapper, is a library that maps classes to database tables)
  • Connection management
  • Transaction mechanics: opening, committing, and rolling back transactions (the business layer decides where each unit of work begins and ends)
  • Query optimization (indexes, query planning)
  • Translation between database records and domain objects

What it is forbidden from doing:

  • Applying business rules
  • Knowing anything about HTTP or the presentation layer
  • Making decisions about what data should be fetched based on business logic
  • Exposing database implementation details to the business layer

The code below is the PostgreSQL implementation of the order repository. Its only task is to move data between database rows and domain objects. It flushes but never commits, because one transaction must cover the whole unit of work, not a single repository call.

Python3
Python3
. . . .

The code below declares the two SQLAlchemy models behind the repository. The unit price column maps to Decimal rather than float, since binary floating point cannot represent most prices exactly.

Python3
Python3
. . . .
Data Access Layer: Repository Pattern
Data Access Layer: Repository Pattern

Part 3: The Four-Layer Variant

Many applications add a fourth layer for external service integrations, sitting alongside or below the data access layer.

The outline below places the integration layer beside the data access layer, directly under the business layer.

Python3
Python3
. . . .

The integration layer handles all communication with external systems: payment processors, email providers, SMS gateways, third-party APIs, and cloud services. Like the data access layer, it implements interfaces defined by the business layer.

The code below is the payment gateway interface, defined in the business layer. The PaymentResult it returns carries success or failure as data, so the business layer never sees provider exceptions.

Python3
Python3
. . . .

The code below implements that interface with the Stripe SDK. The conversion to cents and the Stripe error types stay inside this one class.

Python3
Python3
. . . .

Switching from Stripe to Adyen later mostly requires writing a new class that implements PaymentGatewayInterface. The business layer, the data access layer, and the presentation layer stay unchanged for the charge itself. Some provider details still reach other code, however. This adapter needs a Stripe customer id and a saved card from the wiring, and refunds and webhooks work differently from one provider to the next.

Four-Layer Architecture
Four-Layer Architecture

Part 4: Dependency Injection and Wiring

The layers are defined and their interfaces are clear. Something must wire them together: create concrete instances of each layer and inject the right dependencies at startup. This wiring is done in the composition root, which usually sits at or near the application's entry point.

The code below is the composition root (the one place where the application creates its objects and connects them). It opens one database session per request and commits it only when the request succeeds. Any error rolls it back, including a business error that the controller catches and turns into a 4xx response, so each request is one unit of work by default. It then hands the order service to the presentation layer, where the controller reads it.

Python3
Python3
. . . .

This composition root is the only place in the application where all layers are visible simultaneously. Everything else sees only its own layer and the interface of the layer below.

One transaction per request is only a default. When a use case needs a different boundary, the business layer declares it through a unit of work interface, as the FAQ on transactions shows.

Dependency Injection
Dependency Injection

Part 5: Testing Each Layer Independently

The primary technical payoff of strict layering is independent testability. Each layer can be tested with the layers below it replaced by test doubles.

Testing the Business Logic Layer

Business logic tests run without a database, without HTTP, and without external services. They test domain rules directly.

The code below tests the order service with every dependency replaced by a Mock object. The assertions check the business outcome, such as the final status and total, and nothing about storage or transport.

Python3
Python3
. . . .

These tests run in milliseconds. No database, no network, no external services. The business rules are tested in complete isolation from all infrastructure concerns.

Testing Layers Independently with Mocks
Testing Layers Independently with Mocks

Testing the Presentation Layer

Presentation layer tests verify that HTTP requests are parsed correctly, responses are formatted correctly, and domain exceptions are translated to the right HTTP status codes.

The code below builds a Flask application with a Mock in place of the order service and registers the real blueprint, exactly as the composition root does. Each test then checks only the HTTP status code and the response body.

Python3
Python3
. . . .

Part 6: The Strict Dependency Rule and Its Violations

The Rule

The rule has two parts. Calls point downward only: the presentation layer calls the business layer, the business layer calls the data access layer, and no layer calls upward or skips the layer beneath it. The diagram below shows the allowed call directions and the three ways to break them.

Source-code dependency is a separate question. In classic layering it also points downward, because the business layer imports concrete data access classes. With dependency inversion, as in Part 2, the data access layer imports the repository interface that the business layer owns, so that one source-code dependency points upward. The import is not a violation, because the business layer still knows nothing about the data access layer, and the data access layer still makes no calls into business logic. What the rule forbids is a lower layer calling, or making decisions based on, the layer above it.

The outline below lists the three allowed call directions and the three violations.

Python3
Python3
. . . .
Strict Dependency Rule: Allowed vs. Forbidden Calls
Strict Dependency Rule: Allowed vs. Forbidden Calls

Closed Layers, Open Layers, and the Sinkhole

Mark Richards, in his report Software Architecture Patterns, gives names to the ideas behind this rule. A layer that every request must pass through is a closed layer. A layer that the layer above may skip is an open layer. In the design in this lesson, all three layers are closed, so a request from the presentation layer always reaches the business layer before it reaches the data access layer.

Closed layers give what Richards calls layers of isolation: a change inside one layer does not reach the layers above it, as long as the interface between them stays the same. Opening a layer gives up that isolation for the layers that skip it. Richards' example is a shared services layer placed below the business layer and marked open, so the business layer may skip it and call the data access layer directly.

Closed layers also have a cost. A request that passes through the business layer unchanged, only forwarding a call to a repository, is harmless when it is rare. When most requests look like this, the layers add code and no value, and Richards calls the result the architecture sinkhole anti-pattern. His guideline: if about 20 percent of requests are simple pass-through calls, the design is healthy; if about 80 percent are, the layered style is a poor fit, or some layers should be opened.

This lesson follows one rule. Every layer is closed by default, and a layer is opened only as an explicit, documented decision. If most requests would be pass-through calls, choose a lighter structure for the whole application, as Part 8 describes, instead of letting individual controllers skip the business layer.

Common Violations and Their Costs

The controller that queries the database directly:

The code below reads the orders table from inside a controller.

Python3
Python3
. . . .

The cost: the business rule that users can only view their own orders is not applied. Any authenticated user can view any order. Testing this endpoint requires a database. Adding an audit log for order views requires adding the same code to every controller that queries orders, rather than adding it once to the business layer.

The repository that applies business rules:

The code below selects cancellable orders inside a repository method. The dollar threshold is a business decision, yet it is computed from database rows in the data access layer.

Python3
Python3
. . . .

The cost: the business rule is hidden in a database query. It is the same $10,000 threshold that OrderService uses for manual review: larger orders wait for a person, so only smaller orders may be canceled automatically. Changing the rule requires finding the query and changing the SQL condition. The rule cannot be unit tested without a database. The rule is not visible to engineers reading the business layer code.

The business service that knows about JSON:

The code below shows a service method that accepts and returns dictionaries shaped like the HTTP payload.

Python3
Python3
. . . .

The cost: the business service cannot be used from a CLI command or a background job without constructing a fake JSON payload. The response format (including the "message" field) is a presentation concern mixed into the business layer. Testing the business logic requires asserting on JSON structure rather than domain objects.

Three Common Layered Architecture Violations
Three Common Layered Architecture Violations

Enforcing the Rule with Tools

Code review alone does not keep the layers apart for long, so most teams check the rule automatically on every build. An automated test that checks a property of the architecture, rather than a behavior of the code, is called an architectural fitness function. For Python, the import-linter tool reads a layers contract that lists the layers from top to bottom and fails the build when a lower layer imports a higher one. For Java, ArchUnit expresses the same rule as an ordinary unit test.

The design in this lesson needs one exception in such a contract. The data access layer imports the repository interfaces and domain objects that the business layer owns, which is the dependency inversion described in Part 2. The contract must allow those imports and still forbid every other upward import, such as a repository importing a business service.

Part 7: Advantages and Limitations

Real Advantages

Independent testability. Each layer is testable without the layers below it. Business logic tests run in milliseconds without a database. Presentation tests verify HTTP behavior without business logic execution. The tests for these two layers run quickly and reliably.

Replaceability. Switching from PostgreSQL to DynamoDB is mostly contained in the data access layer, because the business layer depends only on the repository interfaces. Differences in transactions and query patterns often reach those interfaces, however. DynamoDB has no joins and limits what one transaction can cover, so some repository methods and some use cases change as well. In the same way, switching from Stripe to Adyen is mostly contained in the integration layer, and switching from REST to gRPC in the presentation layer. Each replacement is concentrated in one layer, though rarely confined to it completely.

Clear location for every piece of code. New engineers on the team have a clear mental model for where to find code and where to put new code. Business rules live in the business layer. Database queries live in the data access layer. No ambiguity, no searching across the codebase.

Consider adding a rule that gives 10 percent off every order above $200. The calculation goes on the Order class or in OrderService, both in the business layer. If customers can also enter a coupon code, the controller in the presentation layer parses one new request field. If the discount must be stored, the data access layer adds one column to the order model and its mapping. Each change has a known location, and the rule itself is written in exactly one place.

Parallel development. Multiple engineers can work on different layers simultaneously. The interface between layers is the contract. Once interfaces are defined, the team building the business layer and the team building the data access layer can work independently, as long as both keep to the agreed interface.

Real Limitations

Overhead for simple operations. A simple "get user by ID" operation passes through the presentation layer (parse HTTP), the business layer (call repository), and the data access layer (execute query). The result then travels back up through all three layers (translate model to domain object, translate domain object to JSON). The cost at run time is a few extra function calls, which is small next to the database query itself. The larger cost is code: every simple operation needs a controller, a service method, and a repository method. When most operations look like this, the application shows the architecture sinkhole described in Part 6.

The anemic domain model trap. A common failure mode: domain objects become data containers (plain data classes with no methods) and all logic migrates to service classes. Martin Fowler named this anti-pattern the anemic domain model. It is not specific to layering, but layered code makes it more likely, because a separate service layer looks like the obvious place for every rule. The Order object has fields but no methods, and the OrderService contains all logic about orders. Complexity then concentrates in large service classes, which are harder to test and reason about than logic on domain objects. The counter: domain objects should contain the logic that relates to their own state, as the FAQ on domain objects shows.

Layer bypass temptation grows with codebase size. As the codebase grows, the pattern's discipline becomes harder to maintain. Shortcuts are rationalized as exceptions. Over time, a strictly layered architecture degrades into unstructured code (a big ball of mud) unless the team enforces the boundaries automatically, with tools such as those described in Part 6.

Layered Architecture: Advantages vs. Limitations
Layered Architecture: Advantages vs. Limitations

Part 8: When Layered Architecture Is the Right Choice

Use It When

The application has clear separation between request handling, business rules, and data storage. Most web applications and APIs that create, read, update, and delete records (CRUD applications) and also apply business rules to them fit this description.

The team needs clear code organization with multiple engineers. Layered architecture provides unambiguous guidance on where code belongs, which reduces the coordination cost of parallel development.

Testability of business logic is a priority. If the team wants to run business logic tests without a database (fast, reliable unit tests), layered architecture with interface-based dependencies in the business layer enables this directly.

The application may change its database or external service providers. Layered architecture with interfaces at layer boundaries keeps most of such a change inside one layer.

When to Choose Layered Architecture
When to Choose Layered Architecture

Consider Alternatives When

  • The application is genuinely simple CRUD. A five-table application with no significant business rules does not benefit from the extra structure of full layered architecture. Nearly every request would be a pass-through call (the architecture sinkhole from Part 6), so a thin presentation layer calling the ORM directly is the better choice. This is a decision about the whole application, not a shortcut that single controllers take inside a layered one.

  • The domain is highly complex with rich business rules. Highly complex domains with intricate business rules benefit from hexagonal architecture (ports and adapters), which provides stricter isolation of the domain from all infrastructure (both database and HTTP) than standard layered architecture. The Hexagonal (Ports and Adapters) and Clean Architecture/Onion Architecture lessons explain when that move is worth its extra structure.

  • The application is a microservice with one specific capability. A small microservice responsible for one narrow capability may have too little business logic to justify full layering. A simple service-repository pattern (presentation calls a service, service calls a repository) without formal layers may be more appropriate.

FAQs

Q: Is layered architecture the same as MVC (Model-View-Controller)?

MVC and layered architecture are related but not the same thing. MVC is a presentation pattern that describes how the user interface is structured: the Model holds data, the View renders it, and the Controller handles user input and coordinates between them. Layered architecture is a broader architectural pattern that describes how the entire application is structured, not just the UI. In a typical web application, the MVC pattern lives mainly within the presentation layer of a layered architecture. The Controller in MVC maps to the controller in the presentation layer. The Model in MVC is typically the domain object from the business layer. Layered architecture extends beyond what MVC addresses by defining the business logic layer and data access layer beneath the presentation layer.

Q: Can the presentation layer call the data access layer directly for read-only queries?

Not by default. The business layer is a closed layer (see Part 6), so every read passes through it, even when the business method only forwards the call to a repository. The cost at run time is one or two function calls, which is small next to the database query itself.

The reason to keep the extra step is that a read that is simple today often gains a rule later, such as an authorization check or an audit log entry. If some reads bypass the business layer, the new rule must be added in two places, and the place that is missed becomes a security or compliance defect.

A team may still open a layer as an explicit, documented decision, as Part 6 describes. If most requests are pass-through reads, that is the architecture sinkhole, and the better conclusion is that the whole application is simple CRUD and needs a lighter structure (Part 8).

Q: How do you handle cross-cutting concerns like logging, authentication, and caching in a layered architecture?

Cross-cutting concerns span multiple layers and do not belong exclusively to any one of them. The standard approach is to handle them at the layer closest to where they are most relevant, using middleware or decorators rather than embedding them in layer code.

Authentication belongs in the presentation layer: validate the request is from an authenticated user before delegating to the business layer. Logging at the request level (what endpoint was called, by whom, with what result) belongs in the presentation layer as middleware. Logging at the business event level (an order was created, a payment failed) belongs in the business layer as part of the use case.

Caching is more nuanced. Caching of computed results that depend on business rules belongs in the business layer, keyed on domain identifiers. Caching of HTTP responses (full response caching, ETags) belongs in the presentation layer. Caching of database query results belongs in the data access layer, wrapped in the repository implementation.

The code below wraps the database repository in a caching repository that implements the same interface.

Python3
Python3
. . . .

The business layer calls find_by_id and receives a Product domain object. It never knows whether that object came from the database or the cache.

Q: What is the difference between layered architecture and hexagonal architecture (ports and adapters)?

Both styles separate business rules from infrastructure, and the difference is who owns the interfaces. This lesson's design gave the business layer ownership at the data access and integration boundaries, but the presentation layer still sits on top as the only entry point. Hexagonal architecture removes the top and the bottom: the domain owns a port for every external actor, so controllers, command-line tools, and databases all connect to it as adapters.

The outline below contrasts the two shapes.

Python3
Python3
. . . .

The Hexagonal (Ports and Adapters) lesson covers ports, adapters, and the trade-offs in full.

Q: Should domain objects contain business logic or should all logic live in service classes?

Domain objects should contain the logic that governs their own state transitions and invariants. Service classes should contain the logic that orchestrates interactions between multiple domain objects or between domain objects and external systems.

A useful guideline: if a piece of logic only needs to know about one domain object's internal state, it belongs on that domain object. If it needs to coordinate multiple objects or call external services, it belongs in a service.

The code below places the cancellation rule on the Order class and then shows the same rule written in the service, which is the wrong place.

Python3
Python3
. . . .

Putting this rule in the service instead is the first step toward the anemic domain model described in Part 7.

Q: How do you handle transactions that span multiple repository calls in the business layer?

The business layer decides where a unit of work begins and ends, and the data access layer implements the transaction that carries it out. The business layer never calls a database-specific transaction API to do this. The usual tool is the Unit of Work pattern. The request-scoped session in Part 4 is the simplest form of it, with one unit of work per request.

The code below shows a unit of work (one object that groups several repository calls so they succeed or fail together), with its interface in the business layer and its SQLAlchemy implementation in the data access layer. The service decides where the unit of work begins and ends, and the implementation commits or rolls back the session.

Python3
Python3
. . . .

The business layer uses the unit of work through its interface. It knows that the operations are atomic but does not know that the atomicity is provided by a database transaction. A test double can implement UnitOfWorkInterface with in-memory repositories and no actual transaction management.

Q: Can two services in the same layer call each other?

Yes, with discipline. Services within the business layer can call each other when one service's capability is needed by another. An OrderService that needs to check inventory calls an InventoryService. A PaymentService that needs to validate user eligibility calls a UserService.

The risk is circular dependencies: OrderService calls InventoryService calls OrderService. Circular dependencies indicate that the service boundary is wrong: the functionality in the cycle belongs in one service or should be extracted to a third service that both depend on.

The code below shows an acceptable one-way dependency between two services and then a circular one.

Python3
Python3
. . . .

When circular dependencies appear, the shared functionality should be extracted to a domain service that both depend on, or the services should be merged into one.

Q: How does layered architecture handle domain events and async operations?

The business layer defines each event and publishes it through an event bus interface that it owns. The integration layer supplies the implementation: an in-process list for tests, or a message broker such as Kafka in production.

The code below publishes an order created event through an event bus interface. One implementation sends it to Kafka and one keeps events in a list for tests.

Python3
Python3
. . . .

In tests, inject InMemoryEventBus and assert on its published_events list; in production, inject KafkaEventBus. Saving the order and publishing the event are two separate writes, so a crash between them loses the event. Production systems record the event in the same database transaction as the order and let a relay process forward it to the broker (the transactional outbox pattern). The Event-driven Architecture lesson covers brokers, consumers, and the outbox pattern in full.

Key Takeaways

  • Layered architecture organizes code into horizontal layers each with exclusive responsibility: presentation handles HTTP, business logic handles domain rules, data access handles persistence. Each layer is unaware of the implementation details of other layers.

  • The strict dependency rule allows calls to point downward only. The presentation layer calls the business layer. The business layer calls the data access layer. No layer skips another, because every layer is closed by default, and no layer calls upward. Source-code dependencies also point downward in classic layering; dependency inversion turns one of them around, so the data access layer imports the interfaces that the business layer owns.

  • The business layer depends on interfaces, not implementations. Repository interfaces, payment gateway interfaces, and email service interfaces are defined by the business layer and implemented by the data access and integration layers. This is what lets each layer be tested on its own and keeps most replacements inside one layer.

  • Every layer violation has a specific cost. Database queries in controllers bypass business rules and require databases for testing. Business rules in SQL queries are invisible to engineers reading service code and untestable without a database. JSON handling in business services couples business logic to presentation format.

  • Independent testability is the primary technical payoff. Business logic tests run in milliseconds with mock dependencies. Presentation tests verify HTTP behavior without running business logic. Neither requires a running database or external services.

  • The composition root is the only place all layers are visible simultaneously. It creates concrete instances of each layer and wires dependencies together through constructor injection. Everything else sees only the interface of the layer below it.

  • The anemic domain model is a common failure mode, named by Martin Fowler. Domain objects that are pure data containers with all logic in service classes produce large, hard-to-test service classes. Domain objects should contain the logic that governs their own state transitions.

  • Layered architecture is a discipline, not a folder structure. Moving files into folders named "controllers," "services," and "repositories" does not create a layered architecture. The discipline is keeping each layer unaware of the internals of the layers around it, letting it communicate only through defined interfaces, and checking the rule automatically on every build.

  • In a system design interview, describe the layer boundaries, name what each layer is forbidden from knowing, explain how the business layer depends on interfaces (not concrete implementations), and describe how this enables independent testing and replacement. That level of specificity demonstrates architectural understanding rather than vocabulary knowledge.

General
Test Your Knowledge
Check your understanding and reinforce the key concepts covered in this section with a short, targeted assessment.
15 Questions
~23 mins
Your progress is saved automatically

Reading Progress

0%


Vote for new content

On This Page

Know the Vocabulary

Part 1: What Is Layered Architecture

The Core Idea

Where Layering Comes From

Part 2: The Three Layers and Their Responsibilities

Layer 1: The Presentation Layer

Layer 2: The Business Logic Layer

Layer 3: The Data Access Layer

Part 3: The Four-Layer Variant

Part 4: Dependency Injection and Wiring

Part 5: Testing Each Layer Independently

Testing the Business Logic Layer

Testing the Presentation Layer

Part 6: The Strict Dependency Rule and Its Violations

The Rule

Closed Layers, Open Layers, and the Sinkhole

Common Violations and Their Costs

Enforcing the Rule with Tools

Part 7: Advantages and Limitations

Real Advantages

Real Limitations

Part 8: When Layered Architecture Is the Right Choice

Use It When

Consider Alternatives When

FAQs

Key Takeaways