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.

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.
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.
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.
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:
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.
Instead of splitting packages by technology (controller, service, repository), group them by feature:
com.example/
├── item/
│ ├── ItemController.java
│ ├── ItemService.java
│ ├── ItemRepository.java
│ └── Item.java
├── order/
│ ├── OrderController.java
│ ├── OrderService.java
│ └── OrderRepository.java
└── shared/
└── ApiError.javaWith 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.
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
@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.
@Primary beans or proxies.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.
To decouple components within a single application, use Spring Events. A publisher throws an event without knowing who consumes it:
@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:
@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 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:
spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: order-consumer
auto-offset-reset: earliestKafka provides event persistence, replay, and horizontal scaling — the right pattern for the event-driven microservices architecture discussed in episode 14.
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:
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.