Target Audience: Developers learning architecture patterns for the first time Prerequisites: Basic understanding of Spring Boot MVC patterns Estimated Time: About 15 minutes

The most basic and widely used architecture pattern. If you are learning architecture for the first time, start here. Layered architecture divides software horizontally so each layer has a clear role. It follows the simple but powerful rule that each layer can only call the layer below it.

One-Line Summary#

The fundamental principle is dividing code into 4 layers and calling only from top to bottom. This allows each layer to focus on its responsibilities, making the code structure easy to understand.

flowchart TB
    subgraph Layers["4-Layer Structure"]
        P["Presentation Layer<br>(User-facing layer)"]
        A["Application Layer<br>(Flow orchestration layer)"]
        D["Domain Layer<br>(Business rules layer)"]
        I["Infrastructure Layer<br>(Technical details)"]
    end

    P --> A --> D
    I -.->|"Provides implementation"| D

Why Divide Into Layers?#

The reason for dividing into layers is to separate complex systems into manageable units. When each layer has its own responsibility, it is easy to predict the impact of code changes, and collaboration between team members becomes smoother.

Analogy: Company Organization

Think about how work is done in a company:

Company OrganizationSoftware LayerRole
Customer ServicePresentationUnderstanding what the customer wants
Planning TeamApplicationCoordinating processing order
Development TeamDomainDeveloping core features
Infrastructure TeamInfrastructureManaging servers, DBs, etc.

Each team is efficient because they focus on their role, right? The same applies to software.

flowchart TB
    subgraph Company["Company Organization"]
        CS["Customer Service Team<br>(Talks to customers)"]
        PM["Planning Team<br>(Coordinates work)"]
        DEV["Development Team<br>(Core technology)"]
        INFRA["Infrastructure Team<br>(Server, DB management)"]
    end

    CS -->|"Forward request"| PM
    PM -->|"Assign work"| DEV
    DEV -->|"Use infrastructure"| INFRA

Each team has a clear role, so when problems arise, you immediately know where to fix. Software layers operate on the same principle.


4 Layers in Detail#

Layered architecture consists of four major layers. Each layer is clearly distinguished, and upper layers can only call lower layers.

1. Presentation Layer

The presentation layer is the gateway for communicating with users. It receives user input and displays results. It processes HTTP requests or accepts screen input and delivers results as JSON responses or HTML pages. It also validates that input formats are correct.

Users interact with the system only through this layer. When a user clicks a button or submits a form, the presentation layer receives it, converts it to an appropriate format, and passes it to the lower layer.

// Presentation Layer example: Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orderService;  // Application Layer call

    // Receive user request
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {

        // 1. Pass request data to Application Layer
        OrderDto result = orderService.createOrder(
            request.getCustomerId(),
            request.getItems()
        );

        // 2. Return result to user
        return ResponseEntity.ok(OrderResponse.from(result));
    }

    @GetMapping("/{orderId}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable String orderId) {
        OrderDto order = orderService.getOrder(orderId);
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

// Request/Response objects (DTO)
public record CreateOrderRequest(
    String customerId,
    List<OrderItemRequest> items
) {}

public record OrderResponse(
    String orderId,
    String status,
    BigDecimal totalAmount
) {
    public static OrderResponse from(OrderDto dto) {
        return new OrderResponse(dto.orderId(), dto.status(), dto.totalAmount());
    }
}

In the code above, OrderController receives HTTP requests, passes them to OrderService, and converts the results back to HTTP responses. This layer only knows about the HTTP transport protocol details and contains no business logic.

Common Mistake: Putting Business Logic in Presentation

// ❌ Wrong: Discount calculation in Controller
@PostMapping
public ResponseEntity<OrderResponse> createOrder(...) {
    // This logic should not be here!
    if (request.getTotalAmount() > 100000) {
        request.setDiscount(0.1);  // 10% 할인
    }
}

Business logic should be in the Domain Layer.

2. Application Layer

The application layer is the conductor that orchestrates business flows. This layer decides “what” to do but leaves “how” to do it to the Domain Layer. It decides the processing order, manages transactions, and combines Domain Layer objects.

For example, when creating an order, it orchestrates the flow of retrieving customer information, creating the order object, saving it, and sending notifications. While Domain objects handle the actual business rules at each step, the Application Layer decides the execution order.

// Application Layer example: Service
@Service
@Transactional  // Transaction management here
public class OrderService {

    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final PaymentService paymentService;
    private final NotificationService notificationService;

    // Orchestrate order creation "flow"
    public OrderDto createOrder(String customerId, List<OrderItemDto> items) {

        // 1. Retrieve customer
        Customer customer = customerRepository.findById(customerId)
            .orElseThrow(() -> new CustomerNotFoundException(customerId));

        // 2. Create order (business logic inside Order object)
        Order order = Order.create(customer.getId(), toOrderLines(items));

        // 3. Save
        orderRepository.save(order);

        // 4. Send notification
        notificationService.sendOrderCreatedNotification(order);

        // 5. Return result
        return OrderDto.from(order);
    }

    // Order confirmation "flow"
    public void confirmOrder(String orderId) {
        // 1. Retrieve order
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        // 2. Process payment
        paymentService.processPayment(order.getTotalAmount());

        // 3. Confirm order (business logic inside Order)
        order.confirm();

        // 4. Save
        orderRepository.save(order);
    }
}

The code above defines the flow of order creation and confirmation business processes. Each step is executed in order, while the actual business rules are handled by the Order object’s create() and confirm() methods.

Difference Between Application and Domain

// Application Layer: Orchestrate "flow"
public void confirmOrder(String orderId) {
    Order order = orderRepository.findById(orderId);
    paymentService.processPayment(order.getTotalAmount());
    order.confirm();  // Request Domain to "confirm"
    orderRepository.save(order);
}

// Domain Layer: Apply "rules"
public class Order {
    public void confirm() {
        // Business rule: Can only confirm in PENDING state
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Cannot be confirmed in this state");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

3. Domain Layer

The domain layer is the heart of business rules. It is the most important layer, where the “real business logic” resides. It expresses business rules, maintains data consistency, and represents domain concepts.

For example, rules like “VIP customers get 10% discount,” constraints like “order amount must be 0 or more,” and invariants like “orders can only be modified in PENDING state” all reside in this layer. Domain concepts like Order, Customer, and Product are also represented as classes in this layer.

// Domain Layer example: Entity
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderLine> orderLines;
    private OrderStatus status;
    private Money totalAmount;

    // Factory method: Apply business rules
    public static Order create(CustomerId customerId, List<OrderLine> lines) {
        // Rule: Order must have at least 1 item
        if (lines.isEmpty()) {
            throw new IllegalArgumentException("An order requires at least 1 item");
        }

        Order order = new Order();
        order.id = OrderId.generate();
        order.customerId = customerId;
        order.orderLines = new ArrayList<>(lines);
        order.status = OrderStatus.PENDING;
        order.calculateTotal();

        return order;
    }

    // Business logic: Add item
    public void addItem(OrderLine line) {
        // Rule: Items can only be added in PENDING state
        validateModifiable();

        // Rule: If same product, only increase quantity
        orderLines.stream()
            .filter(existing -> existing.isSameProduct(line))
            .findFirst()
            .ifPresentOrElse(
                existing -> existing.increaseQuantity(line.getQuantity()),
                () -> orderLines.add(line)
            );

        calculateTotal();
    }

    // Business logic: Confirm order
    public void confirm() {
        // Rule: Can only confirm in PENDING state
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException(
                "Cannot confirm order. Current state: " + status
            );
        }

        // Rule: Check minimum order amount
        if (this.totalAmount.isLessThan(Money.of(1000))) {
            throw new IllegalStateException("Minimum order amount is 1,000 won");
        }

        this.status = OrderStatus.CONFIRMED;
    }

    // Business logic: Cancel order
    public void cancel() {
        // Rule: Can only cancel before shipping starts
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("Orders that have started shipping cannot be cancelled");
        }

        this.status = OrderStatus.CANCELLED;
    }

    // Internal logic
    private void calculateTotal() {
        this.totalAmount = orderLines.stream()
            .map(OrderLine::getAmount)
            .reduce(Money.ZERO, Money::add);
    }

    private void validateModifiable() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Cannot be modified in this state");
        }
    }
}

In the code above, the Order class encapsulates all business rules related to orders. Externally, state can only be changed through Order’s public methods, and each method only changes state after validating business rules.

// Domain Layer: Value Object
public record Money(BigDecimal amount) {

    public static final Money ZERO = new Money(BigDecimal.ZERO);

    public Money {
        // Invariant: Amount must be 0 or more
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount must be 0 or more");
        }
    }

    public static Money of(long amount) {
        return new Money(BigDecimal.valueOf(amount));
    }

    public Money add(Money other) {
        return new Money(this.amount.add(other.amount));
    }

    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)));
    }

    public boolean isLessThan(Money other) {
        return this.amount.compareTo(other.amount) < 0;
    }
}

Money is an example of a Value Object. It is immutable, compared by value, and self-validates its invariant (amount must be 0 or more).

Common Mistake: Anemic Domain

// ❌ Wrong: Entity with only data and no logic
public class Order {
    private String id;
    private String status;
    private BigDecimal total;

    // Only getters and setters...
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

// Logic is in Service
public class OrderService {
    public void confirm(Order order) {
        if (order.getStatus().equals("PENDING")) {
            order.setStatus("CONFIRMED");  // Should not do this!
        }
    }
}

Business logic should be inside the Entity!

4. Infrastructure Layer

The infrastructure layer handles technical details. It is responsible for all communication with external systems including database access, external API calls, message sending, and file storage. Technical tools like JPA, MyBatis, REST Client, Kafka, and Email reside in this layer.

Implementations in this layer implement Domain Layer interfaces. For example, the OrderRepository interface is defined in the Domain Layer, while the JpaOrderRepository implementation is in the Infrastructure Layer.

// Infrastructure Layer: Repository implementation
@Repository
public class JpaOrderRepository implements OrderRepository {

    private final OrderJpaRepository jpaRepository;  // Spring Data JPA
    private final OrderMapper mapper;

    @Override
    public void save(Order order) {
        // Domain -> JPA Entity conversion
        OrderEntity entity = mapper.toEntity(order);
        jpaRepository.save(entity);
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        // JPA Entity -> Domain conversion
        return jpaRepository.findById(id.getValue())
            .map(mapper::toDomain);
    }
}

// JPA Entity (Infrastructure only)
@Entity
@Table(name = "orders")
public class OrderEntity {
    @Id
    private String id;
    private String customerId;
    private String status;
    private BigDecimal totalAmount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderLineEntity> orderLines;

    // getter, setter (used only in Infrastructure)
}

The code above converts Domain Order objects into JPA Entities for database storage. The Domain Layer knows nothing about database technology and only uses the OrderRepository interface.

// Infrastructure Layer: External API integration
@Component
public class PaymentGatewayClient implements PaymentService {

    private final RestTemplate restTemplate;

    @Override
    public PaymentResult processPayment(Money amount) {
        PaymentRequest request = new PaymentRequest(amount.getAmount());

        ResponseEntity<PaymentResponse> response = restTemplate.postForEntity(
            "https://payment-api.example.com/pay",
            request,
            PaymentResponse.class
        );

        return toPaymentResult(response.getBody());
    }
}

Communication with external payment APIs is also handled in the Infrastructure Layer. The Domain Layer only knows the PaymentService interface and does not know which payment system is actually used.


Package Structure#

When applying layered architecture to a Java project, package structure is important. Separate each layer into distinct packages so layers are clearly distinguished physically as well.

Basic Structure

Below is the package structure for the order domain organized in layers. Each layer is separated into independent packages, making it easy for code readers to identify which layer a class belongs to.

com.example.order/
├── presentation/              # Presentation Layer
│   ├── OrderController.java
│   ├── CreateOrderRequest.java
│   └── OrderResponse.java
│
├── application/               # Application Layer
│   ├── OrderService.java
│   └── OrderDto.java
│
├── domain/                    # Domain Layer
│   ├── Order.java
│   ├── OrderLine.java
│   ├── OrderId.java
│   ├── OrderStatus.java
│   ├── Money.java
│   └── OrderRepository.java   # Interface (구현은 Infrastructure에)
│
└── infrastructure/            # Infrastructure Layer
    ├── persistence/
    │   ├── JpaOrderRepository.java
    │   ├── OrderEntity.java
    │   └── OrderMapper.java
    └── external/
        └── PaymentGatewayClient.java

The presentation package contains controllers and DTOs, the application package contains services and application DTOs, the domain package contains entities and value objects, and the infrastructure package contains Repository implementations and external integration code.

Dependency Direction

Dependencies always point from top to bottom only. In the diagram below, arrows indicate dependency direction. Presentation depends on application, application depends on domain, and infrastructure also depends on domain. But domain depends on nothing.

flowchart TB
    P["presentation"]
    A["application"]
    D["domain"]
    I["infrastructure"]

    P --> A
    A --> D
    I --> D

This makes the domain layer the most stable layer, and technical changes (e.g., switching from JPA to MyBatis) do not affect the domain.


Dependency Inversion (DIP)#

“Domain does not depend on Infrastructure” may sound strange. How can it not depend when using a Repository? The secret lies in interfaces.

The Secret: Interfaces

Define Repository interfaces in the Domain Layer, and implement them in the Infrastructure Layer. This way, Domain does not need to know about specific implementations and only uses interfaces.

flowchart LR
    subgraph Domain["Domain Layer"]
        O["Order"]
        RI["OrderRepository<br>(Interface)"]
    end

    subgraph Infrastructure["Infrastructure Layer"]
        JR["JpaOrderRepository<br>(Implementation)"]
    end

    O -->|"uses"| RI
    JR -->|"implements"| RI

In the diagram above, Order uses the OrderRepository interface, and JpaOrderRepository implements that interface. The dependency direction is inverted.

// Domain Layer: Interface definition
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

// Domain Layer: Service uses only interfaces
@Service
public class OrderService {
    private final OrderRepository orderRepository;  // Interface type

    public void createOrder(...) {
        orderRepository.save(order);  // Does not know specific implementation
    }
}

// Infrastructure Layer: Interface implementation
@Repository
public class JpaOrderRepository implements OrderRepository {
    // Implemented using JPA
}

This way, Domain only needs to know the OrderRepository interface, and even if JPA is replaced with MyBatis, no Domain code needs to change. It is also convenient for testing, as you can use Mock Repositories.


Pros and Cons of Layered#

Layered architecture is simple and intuitive but has some pros and cons. Depending on project characteristics, advantages may outweigh disadvantages or vice versa.

Advantages

The advantages of layered architecture mainly lie in being easy to understand and apply. The table below summarizes the key advantages.

AdvantageDescription
Easy to understandIntuitive top-down flow
Clear rolesEach layer’s purpose is obvious
Quick startCan be applied immediately without complex setup
Team collaborationDivision of labor: “you handle Controller, I handle Service”

Thanks to the intuitive top-to-bottom flow, even new developers can easily understand it. With clearly defined roles for each layer, there is less need to worry about where to write code. It can be applied immediately without complex configuration or additional tools, enabling quick project starts. Team collaboration is also efficient as members can divide work by layer.

Disadvantages

However, layered architecture has some disadvantages. These drawbacks can become burdensome as projects grow.

DisadvantageDescription
Forced layer traversalEven simple queries must pass through all layers
Technology dependencyInfrastructure changes can affect Domain
Testing difficultyDifficult to test without Mocks

Even simple data retrieval must pass through all layers, potentially creating unnecessary code. Attaching JPA annotations directly to Domain Entities creates technology dependencies that make future changes difficult. Testing can be cumbersome as all lower layers must be mocked.


Common Mistakes#

Let us look at common mistakes when applying layered architecture. Avoiding these mistakes leads to cleaner code.

1. Layer Skipping

Skipping layers violates the core principle of layered architecture. Each layer should only call the layer directly below it; layers should not be skipped.

// ❌ Controller directly accessing Repository
@RestController
public class OrderController {
    @Autowired
    private OrderRepository orderRepository;  // Application Layer skipped!

    @GetMapping("/{id}")
    public Order getOrder(@PathVariable String id) {
        return orderRepository.findById(id);  // Returns directly without validation or conversion
    }
}

The code above has the Controller directly calling the Repository, skipping the Application Layer. This eliminates opportunities to apply business logic and blurs responsibilities between layers.

// ✅ Correct approach: Pass through Application Layer
@RestController
public class OrderController {
    private final OrderService orderService;  // Application Layer

    @GetMapping("/{id}")
    public OrderResponse getOrder(@PathVariable String id) {
        OrderDto dto = orderService.getOrder(id);  // Pass through Service
        return OrderResponse.from(dto);
    }
}

The correct approach is for Controller to call Service, and Service to call Repository. This allows each layer to fulfill its role.

2. Technical Code in Domain

Attaching framework annotations like JPA or Spring to Domain Entities ties the Domain to technology. To maintain a pure Domain model, create separate Entities in Infrastructure.

// ❌ JPA annotations on Domain Entity
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(cascade = CascadeType.ALL)
    private List<OrderLine> lines;
}

The code above has the Domain Entity directly depending on JPA. If you want to switch JPA to another technology later, all Domain code must be modified.

To maintain a pure Domain model, keep only pure Java objects in Domain and create separate JPA Entities in Infrastructure. Then use Mappers to convert between Domain objects and JPA Entities.

3. Circular Dependencies

Circular dependencies occur when two or more Services depend on each other. This can cause compile errors or runtime problems.

// ❌ Circular dependency
// OrderService → PaymentService → OrderService

@Service
public class OrderService {
    private final PaymentService paymentService;
}

@Service
public class PaymentService {
    private final OrderService orderService;  // Circular!
}

The code above creates a circular structure where OrderService and PaymentService depend on each other. This structure makes code hard to understand and testing difficult.

// ✅ Resolve with events
@Service
public class OrderService {
    private final EventPublisher eventPublisher;

    public void confirmOrder(String orderId) {
        // ...
        eventPublisher.publish(new OrderConfirmedEvent(order));
    }
}

@Component
public class PaymentEventHandler {
    @EventListener
    public void onOrderConfirmed(OrderConfirmedEvent event) {
        // Process payment
    }
}

A good way to resolve circular dependencies is using events. OrderService publishes an event, and PaymentEventHandler receives and processes that event. This way, the two services do not directly depend on each other.


Testing Strategy#

In layered architecture, each layer can be tested independently. The appropriate testing method varies by layer.

1. Domain Layer Test (Easiest)

Since the Domain Layer has no external dependencies, you only need to test pure logic. No Mocks needed, and test execution is fast.

class OrderTest {

    @Test
    void 주문_생성_시_총액이_계산된다() {
        // Given
        List<OrderLine> lines = List.of(
            new OrderLine(ProductId.of("P1"), 2, Money.of(10000)),
            new OrderLine(ProductId.of("P2"), 1, Money.of(5000))
        );

        // When
        Order order = Order.create(CustomerId.of("C1"), lines);

        // Then
        assertThat(order.getTotalAmount()).isEqualTo(Money.of(25000));
    }

    @Test
    void PENDING_상태에서만_확정_가능하다() {
        Order order = createPendingOrder();

        order.confirm();

        assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
    }

    @Test
    void 배송_시작된_주문은_취소할_수_없다() {
        Order order = createShippedOrder();

        assertThrows(IllegalStateException.class, () -> order.cancel());
    }
}

All the tests above are pure unit tests. They verify only the Order class logic without databases or external services.

2. Application Layer Test (Using Mocks)

Since the Application Layer combines multiple lower layers, test with Mocks. Replace Repositories and external services with Mocks for fast testing.

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void 주문_생성_성공() {
        // Given
        String customerId = "customer-1";
        List<OrderItemDto> items = List.of(
            new OrderItemDto("product-1", 2, 10000)
        );

        // When
        OrderDto result = orderService.createOrder(customerId, items);

        // Then
        verify(orderRepository).save(any(Order.class));
        verify(notificationService).sendOrderCreatedNotification(any());
        assertThat(result.orderId()).isNotNull();
    }
}

The test above verifies OrderService’s flow orchestration logic. It replaces Repository and NotificationService with Mocks to test without an actual database or notification service.

3. Infrastructure Layer Test (Integration Test)

Since the Infrastructure Layer communicates with actual databases and external systems, perform integration tests. Using Spring Boot tools like @DataJpaTest is convenient.

@DataJpaTest
class JpaOrderRepositoryTest {

    @Autowired
    private OrderJpaRepository jpaRepository;

    private JpaOrderRepository repository;

    @BeforeEach
    void setUp() {
        repository = new JpaOrderRepository(jpaRepository, new OrderMapper());
    }

    @Test
    void 주문을_저장하고_조회한다() {
        // Given
        Order order = createTestOrder();

        // When
        repository.save(order);
        Optional<Order> found = repository.findById(order.getId());

        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getId()).isEqualTo(order.getId());
    }
}

The test above uses actual JPA and a database (usually an in-memory DB like H2) to verify that the Repository implementation works correctly.


When Should You Use Layered?#

Layered architecture is not suitable for all situations. Suitability varies depending on project characteristics and team circumstances.

Suitable Cases

Layered architecture works particularly well in the following situations. In early project stages, simple layered may be more suitable than complex architecture. When the team has little architecture pattern experience, easy-to-understand layered is also good.

If business logic is not complex and the application is mainly simple CRUD, layered is sufficient. It is also suitable for MVPs and prototypes that need rapid development.

Unsuitable Cases

On the other hand, layered may be unsuitable in the following situations. When there are many external system integrations, consider hexagonal architecture. When there is complex domain logic, onion architecture may be more suitable.

For large teams or long-term projects, stricter rules like clean architecture may be needed.

Best Practice: Which Systems Fit?

System TypeFitReason
Startup MVPVery suitableRapid development, low learning curve
Internal Admin ToolSuitableModerate complexity, easy maintenance
Simple REST APISuitableCRUD-focused, clear layer separation
Monolith Starting PointSuitableCan evolve to hexagonal later
Systems with Many IntegrationsUnsuitableHexagonal recommended
Complex DomainUnsuitableOnion/Clean recommended
Large EnterpriseUnsuitableStricter rules needed

Key Summary#

Layered Architecture Key Summary
LayerRoleExample
PresentationUser request/response handlingController, Request/Response DTO
ApplicationBusiness flow orchestrationService (transaction management)
DomainCore business rulesEntity, Value Object
InfrastructureTechnical detailsRepository impl, external API

Core Rules:

  1. Call only from top to bottom (Presentation -> Application -> Domain)
  2. Domain depends on nothing (pure business logic)
  3. Repository interfaces in Domain, implementations in Infrastructure

Evolving to the Next Level#

Once you are comfortable with layered, you can move to more advanced patterns as needed. Gradual improvement is recommended.

flowchart LR
    A["Layered<br>(Current)"]
    B["Domain Separation"]
    C["Hexagonal"]

    A -->|"1. Extract Repository Interface"| B
    B -->|"2. Introduce Port/Adapter"| C

Step 1: Move Repository Interface to Domain

First, move the Repository interface from Infrastructure to Domain. This prevents Domain from depending on Infrastructure.

// Before: Infrastructure에 있던 것을
// After: Domain으로 이동
package com.example.domain;

public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

Step 2: Abstract More External Integrations as Interfaces

Abstract all external service integrations as interfaces. Going through this process naturally evolves into hexagonal architecture.


Next Steps#