This episode covers Quarkus application architecture: modular architecture with feature-based organization, domain-driven design and hexagonal architecture, event-driven architecture with reactive messaging, as well as CQRS, event sourcing, and publish/subscribe patterns.

The larger an application gets, the more expensive a wrong structure becomes. Disorganized code becomes hard to change, hard to test, and hard for a new team to understand. A good architecture isn't a luxury — it's an investment in the project's longevity.
Episode 18 covers architecture and design patterns for Quarkus applications: modular architecture with feature-based organization, domain-driven design and hexagonal architecture, event-driven architecture with reactive messaging, as well as CQRS, event sourcing, and publish/subscribe patterns.
Instead of grouping files by type (all controllers in one folder, all services in another), group them by feature. This makes each feature self-contained and easy to change.
src/main/java/com/example/
├── order/
│ ├── Order.java
│ ├── OrderRepository.java
│ ├── OrderService.java
│ └── OrderResource.java
├── customer/
│ ├── Customer.java
│ ├── CustomerRepository.java
│ └── CustomerResource.java
└── common/
├── ErrorResponse.java
└── ValidationExceptionMapper.javaEach feature contains its entity, repository, service, and resource in one package. The common folder holds things that are truly cross-feature. Run ./mvnw quarkus:dev to see the Dev UI, which lists all beans per package.
A change in one feature doesn't disturb other features. Teams can work in parallel on different features with minimal conflicts. Quarkus processes every package uniformly, so this separation adds no overhead.
Domain-Driven Design (DDD) puts the domain model at the center. Important concepts: bounded context, aggregate, entity, and value object. Repositories and services are separated from the transport layer.
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
@Table(name = "customers")
public class Customer extends PanacheEntity {
public String nama;
public String email;
public void ubahEmail(String emailBaru) {
this.email = emailBaru;
}
}Hexagonal architecture (ports and adapters) separates the application core from the outside world. A port is an interface defined by the domain; an adapter is a technical implementation (REST, database, messaging). Quarkus makes this pattern easy with CDI interfaces:
public interface CustomerPort {
Customer simpan(Customer customer);
}
@ApplicationScoped
public class CustomerRepositoryAdapter implements CustomerPort {
@Override
public Customer simpan(Customer customer) {
return customer.persistAndReturnSelf();
}
}The domain calls CustomerPort, not caring whether the implementation is Hibernate, another database, or a mock. Swapping implementations just means swapping the adapter — the domain code doesn't change.
Event-driven architecture separates producers from consumers. A producer only publishes events; consumers decide what to do. In Quarkus, this is realized with Reactive Messaging (episode 15).
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
@ApplicationScoped
public class OrderEventPublisher {
@Inject
@Channel("order-created")
Emitter<OrderCreated> emitter;
public void terbitkan(OrderCreated event) {
emitter.send(event);
}
}After an order is created, the service calls terbitkan(...). Consumers (for example a notification service) listen to order-created and send emails — without direct coupling.
Connector configuration makes events flow through Kafka:
mp.messaging.outgoing.order-created.connector=smallrye-kafka
mp.messaging.outgoing.order-created.topic=order-createdEvent-driven designs make your application more resilient: if a consumer is down, events stay queued in the broker and are processed when the consumer returns.
CQRS (Command Query Responsibility Segregation) separates the write model from the read model. Writes use commands and a transactional database; reads use optimized queries:
@ApplicationScoped
public class OrderCommandService {
@Transactional
public void buatOrder(BuatOrderCommand cmd) { ... }
}
@ApplicationScoped
public class OrderQueryService {
public List<OrderView> cariOrder(String keyword) { ... }
}CQRS gives you flexibility: the read model can use views, caches, or a read-only database without affecting write integrity.
Event sourcing stores every state change as an event, not just the final state. The current state is computed by replaying events. It suits domains that need a complete audit trail, such as finance.
Publish/subscribe lets a single event be consumed by many subscribers. In Reactive Messaging, use different groups and topics:
mp.messaging.incoming.order-events.connector=smallrye-kafka
mp.messaging.incoming.order-events.topic=orders
mp.messaging.incoming.order-events.group.id=notifikasi-groupEach service uses its own group id, so a single orders event can trigger notifications, auditing, and analytics independently.
Episode 18 equips you with an architecture blueprint: feature-based organization for modularity, DDD and hexagonal architecture to separate the domain from technology, event-driven architecture with reactive messaging for decoupling, as well as CQRS, event sourcing, and publish/subscribe for advanced scenarios.
Key takeaways:
In episode 19 we'll cover modern tooling and build automation — the Quarkus CLI, Maven plugin, and Gradle plugin, Dev Services for databases and observability, live coding and the Dev UI, as well as reproducible artifacts and native image pipelines.