Skip to content
Leo Jung
Go back

[DDD Intro #19] Layered Architecture: Protecting the domain layer

Edit page

When we try to implement DDD, architecture soon enters the conversation. Names such as Presentation Layer, Application Layer, Domain Layer, and Infrastructure Layer appear. We split packages, decide dependency direction, and separate responsibilities by layer.

But it is not enough to understand Layered Architecture as simply classifying code neatly. In DDD, the most important reason to split layers is to protect the domain model.

The domain model should contain business language and rules. But technical details such as web frameworks, databases, messaging, and external APIs can always contaminate that model. Layered Architecture creates boundaries so that the domain model is not pulled by these technical concerns.

Layers are an agreement for separating responsibilities

In DDD, layers are generally divided like this:

Presentation Layer
Application Layer
Domain Layer
Infrastructure Layer

The Presentation Layer receives requests from users or external systems. HTTP Controllers, GraphQL Resolvers, and Message Listeners belong here.

The Application Layer coordinates use cases. It converts requests into Commands, loads Aggregates through Repositories, calls domain objects, and manages transactions.

The Domain Layer is the core of the domain model. Entity, Value Object, Aggregate, Domain Service, Domain Event, and Repository interfaces can belong here.

The Infrastructure Layer handles technical implementations. Database access, external API calls, message publishing, file storage, and email delivery are details here.

This division is not an absolute law. It can differ depending on project size, language, and framework. What matters is that the team clearly understands what responsibility each layer has.

The domain layer should be at the center

The most important layer in DDD is the Domain Layer. But in real code, the domain layer is often placed in the weakest position.

Controllers force request formats. Databases force table structures. ORMs force default constructors and proxy constraints. External APIs force their response models.

If these influences enter domain objects as they are, the model starts speaking the language of technology rather than the language of the domain.

@Entity
@Table(name = "tb_subs")
public class Subscription {
    @Column(name = "stat_cd")
    private String statCd;

    public void applyPgResponse(PgResponse response) {
        if (response.getResultCode().equals("0000")) {
            this.statCd = "A";
        }
    }
}

This code is an example where persistence structure and an external payment gateway response have entered the domain model. The language of the subscription domain is faint, and technical details are strongly visible.

Layered Architecture is a structure for pushing these dependencies outward.

Dependency direction matters

More important than splitting layers is dependency direction. If the domain layer directly depends on frameworks or the infrastructure layer, it is hard to protect.

The desirable direction is generally this:

Presentation -> Application -> Domain
Infrastructure -> implementations of abstractions in Domain or Application

The Application Layer uses the Domain Layer. The Infrastructure Layer can implement Repository interfaces defined by the Domain Layer. But the Domain Layer should not know concrete implementations in Infrastructure.

// domain
public interface SubscriptionRepository {
    Optional<Subscription> findById(SubscriptionId id);
    void save(Subscription subscription);
}

// infrastructure
public class JpaSubscriptionRepository implements SubscriptionRepository {
    // implementation using JPA
}

In this structure, the domain model does not know the JPA implementation. How storage is implemented is an outside concern.

Application Layer connects the domain and the outside world

The Application Layer is a coordinator that connects the domain layer with the outside world.

@Transactional
public void cancel(CancelSubscriptionCommand command) {
    Subscription subscription = subscriptionRepository.findById(command.subscriptionId());
    subscription.cancelByMember(command.reason(), clock.now());
    subscriptionRepository.save(subscription);
}

This layer receives a request and makes domain objects work. But it is better for it not to directly process domain rules.

Without an Application Layer, Controllers directly combine domain objects, Repositories, and external APIs. Conversely, if the Application Layer makes too many domain judgments, the Domain Layer becomes anemic.

A good Application Layer coordinates flow between external requests and the domain model, while leaving domain judgment to the domain layer.

Infrastructure is a detail

Databases, message brokers, external APIs, and file systems are important. But from the domain perspective, they are details.

In the subscription domain, what matters is the rule “an expired subscription cannot be renewed.” Whether it is stored in MySQL or PostgreSQL, whether events are published through Kafka or HTTP, is not the essence of the domain rule.

Of course, technology choices greatly affect system quality. But the domain model should not be dependent on technology choices. The Infrastructure Layer implements the functions the domain model needs, but should not erode domain language.

public interface PaymentPort {
    PaymentResult pay(PaymentRequest request);
}

The domain or application only needs to understand PaymentResult. Actual response codes from the payment gateway are translated in Infrastructure.

Boundaries can collapse even when layers are split

Splitting packages does not guarantee Layered Architecture. The following things often happen:

Controller directly calls setters on Entity.
Application Service contains every domain conditional.
Domain Layer imports external API DTOs.
Infrastructure's JPA Entity decides every domain rule.
Common Util classes bypass domain policy.

Layers are not folder structure. They are an agreement about dependencies and responsibilities. If this agreement breaks, the architecture collapses even if the packages remain.

In code review, we should often ask:

Which layer is responsible for this rule?
Does the domain layer know technical details?
Is Application Service making too many domain judgments?
Is the Infrastructure model replacing the domain model?

Realistic compromise is necessary

When talking about Layered Architecture, it can feel like every technical dependency must be removed perfectly. But in reality, compromise is necessary.

In a small service, completely separating the domain model and JPA Entity can be costly. Creating ports and adapters for every external integration can also be excessive.

What matters is judging which compromises damage the Core Domain. In a simple CRUD area, a technology-friendly structure may be enough. In the Core Domain, stronger boundaries are needed so that the domain model is not pulled by technical structure.

Architecture is not a religion. The purpose is to protect the domain.

Closing

Layered Architecture is not a way to divide code into four folders. It is a structure that places the domain model at the center and separates responsibilities so that external requests and technical details do not erode domain rules.

The Presentation Layer receives requests, the Application Layer coordinates use cases, the Domain Layer contains business language and rules, and the Infrastructure Layer implements technical details.

This distinction matters because the domain model must survive for a long time. Frameworks can change, databases can change, and external APIs can change. But the language and rules of the core domain should remain at the center of the system.

A good Layered Architecture does not isolate the domain model for its own sake. It creates a space where the domain model can work in its own language.


Edit page
Share this post:

Previous Post
[DDD Intro #20] Hexagonal Architecture: A structure that places the domain at the center
Next Post
[DDD Intro #18] Specification and Policy: Modeling conditions and policies