Level 3 · 30 min
Architectural Patterns
Architectural patterns operate at a higher level than GoF patterns — they structure entire systems rather than single classes. Repository, Event Bus, CQRS, and Saga solve recurring architectural challenges in enterprise systems.
Repository Pattern
Repository abstracts data access behind a collection-like interface. The domain model works with domain objects, not SQL rows or documents. The repository translates between domain objects and the persistence format. Benefits: testability (swap real DB with in-memory fake), single place for all queries for an aggregate, freedom to change the persistence technology.
Event Bus Pattern
Event Bus decouples event publishers from subscribers. Publishers emit events without knowing who handles them. Subscribers register for event types without knowing who emits them. Synchronous event bus: handlers called inline (simple, but a failing handler blocks the publisher). Asynchronous event bus: handlers called on separate threads or via message broker (decoupled, but requires idempotency). The Repository pattern addresses the same fundamental problem as GoF's Facade: hiding subsystem complexity behind a clean interface. As GoF states about Facade: "Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use." (Gamma et al., Design Patterns, p.185). Repository is a domain-specific Facade over the persistence subsystem, exposing a collection-like interface (findById, save, delete) that hides SQL, ORM mapping, connection management, and caching. Eric Evans (Domain-Driven Design, p.151) extends GoF: the Repository "acts like an in-memory collection of all objects of a type... provides objects to the domain model while hiding details of the database." In Spring Data, JpaRepository is the canonical implementation — the interface is pure domain language (findByCustomerIdAndStatus), and Spring generates the query automatically.
CQRS and Saga
CQRS (Command Query Responsibility Segregation) uses separate models for writes (commands) and reads (queries). The write model enforces invariants; the read model is optimized for queries with denormalized projections. Saga manages distributed transactions across services using a sequence of local transactions with compensations for rollback.
Code example
// Repository pattern
interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomer(CustomerId customerId);
void save(Order order);
void delete(OrderId id);
}
// JPA implementation
class JpaOrderRepository implements OrderRepository {
private final JpaRepository<OrderEntity, UUID> jpa;
private final OrderMapper mapper;
public Optional<Order> findById(OrderId id) {
return jpa.findById(id.value()).map(mapper::toDomain);
}
public void save(Order order) {
jpa.save(mapper.toEntity(order));
}
}
// In-memory for tests
class InMemoryOrderRepository implements OrderRepository {
private final Map<OrderId, Order> store = new HashMap<>();
public Optional<Order> findById(OrderId id) { return Optional.ofNullable(store.get(id)); }
public void save(Order order) { store.put(order.id(), order); }
}