Learn Spring Boot - Modular Architecture & Patterns
Episode 18 of 24

Learn Spring Boot - Modular Architecture & Patterns

This episode covers a healthy application structure: clean, hexagonal, and onion architectures; feature module structure and package-by-feature; popular design patterns like service layer and repository; and event-driven architecture with Spring Events and Kafka.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Long-lived applications don't die because of technology — they die because a bad structure makes every change harder. Episode 18 covers modular architecture and design patterns that keep a Spring Boot application maintainable.

You'll learn about clean, hexagonal, and onion architectures; feature-based module structure; design patterns commonly used in Spring; and event-driven architecture with Spring Events and Kafka to decouple components even further.

Clean, Hexagonal, and Onion Architectures

A Shared Concept: Dependencies Point Inward

These three architectures share the same principle: business logic sits at the center, and dependencies always point inward — the domain doesn't depend on technical details like frameworks or databases.

Clean architecture layers
Domain / Use Case
  -> Application Services
  -> Adapters (controller, repository, external API)

Clean Architecture separates entities and use cases from controllers and gateways. Hexagonal (Ports and Adapters) exposes the domain through ports (interfaces) implemented by adapters (technical implementations). Onion Architecture is a similar concept with layers centered on the domain.

Applying It in Spring Boot

In Spring Boot, the principle means: the domain layer must not import Spring MVC or JPA. Controllers and repositories become adapters living in the outer layer:

Domain port as an interface
public interface ItemRepositoryPort {
    Optional<Item> findById(Long id);
    Item save(Item item);
}

The domain only depends on ItemRepositoryPort. The JPA implementation — JpaItemRepositoryAdapter — fulfills that port and can be swapped without changing the domain. That's the essence of hexagonal architecture.

Feature Modules and Package-by-Feature

Package-by-Feature

Instead of splitting packages by technology (controller, service, repository), group them by feature:

Package-by-feature structure
com.example/
├── item/
│   ├── ItemController.java
│   ├── ItemService.java
│   ├── ItemRepository.java
│   └── Item.java
├── order/
│   ├── OrderController.java
│   ├── OrderService.java
│   └── OrderRepository.java
└── shared/
    └── ApiError.java

With this structure, all files related to one feature live in a single package — a feature change only touches one area. Package-by-feature makes a large application far easier to navigate than package-by-layer.

Multi-Module Projects

For very large codebases, split into separate Maven/Gradle modules — for example domain, application, infrastructure, and web. The details will be covered in episode 19 about build automation.

Service Layer and Repository

  • Service Layer — separates business logic from controllers and repositories; holds application use cases and transactions.
  • Repository — separates data access from the domain; provides an abstract interface.
Service layer wrapping a repository
@Service
@Transactional
public class ItemService {
 
    private final ItemRepository repository;
 
    public ItemService(ItemRepository repository) {
        this.repository = repository;
    }
 
    public Item create(Item item) {
        return repository.save(item);
    }
}

Both patterns are already Spring idioms — you've been using them since episodes 4 and 6. Transactions live in the service layer because that's where the application's unit-of-work boundary is.

Decorator and Template Method

  • Decorator — wraps an object to add behavior without changing the original class; in Spring often via @Primary beans or proxies.
  • Template Method — defines an algorithm skeleton in a base class; JdbcTemplate and RestTemplate are Spring examples of this pattern.

Spring itself relies heavily on the proxy pattern: transactions, security, and caching work through proxies that wrap the original bean. Understanding this helps you predict the behavior of annotations like @Transactional.

Event-Driven Architecture

Spring Events for Internal Communication

To decouple components within a single application, use Spring Events. A publisher throws an event without knowing who consumes it:

Publishing an event
@Service
public class OrderService {
 
    private final ApplicationEventPublisher publisher;
 
    public OrderService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
 
    public Order placeOrder(Order order) {
        Order saved = repository.save(order);
        publisher.publishEvent(new OrderPlacedEvent(saved));
        return saved;
    }
}

Consumers catch the event with @EventListener:

Listening for an event
@Component
public class EmailNotifier {
 
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        // kirim email, tanpa bergantung pada OrderService
    }
}

OrderService doesn't need to know about EmailNotifier — the communication is reversed through the event. This breaks direct dependencies between components. To see it in action, run the application with ./mvnw spring-boot:run and trigger an order; the listener's log output will appear.

Spring Events vs Message Broker

Spring Events run in a single process — suitable for internal decoupling. For communication between services or when you need reliability, use a message broker like Kafka or RabbitMQ:

Kafka consumer configuration
spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: order-consumer
      auto-offset-reset: earliest

Kafka provides event persistence, replay, and horizontal scaling — the right pattern for the event-driven microservices architecture discussed in episode 14.

Closing

Episode 18 equipped you with the architecture and patterns that keep an application healthy: clean, hexagonal, and onion architectures; package-by-feature and modules; the service layer, repository, and decorator design patterns; and event-driven architecture with Spring Events and Kafka.

Key takeaways:

  • Business logic at the center; dependencies always point inward.
  • Ports and adapters keep the domain independent of frameworks and databases.
  • Package-by-feature is easier to maintain than package-by-layer.
  • The service layer and repository are foundational patterns used throughout Spring.
  • Spring Events decouple components within a single process.
  • Kafka and RabbitMQ handle inter-service communication with high reliability.

In the next episode, episode 19, we'll discuss modern tooling and build automation — a comparison of Maven vs Gradle Kotlin DSL, the Spring Boot plugin and BOM dependency management, live reload with Spring DevTools, and reproducible builds and multi-module projects.