We closed the previous module with an observation: the GoF catalog was written for objects living together inside a single process, yet today's software is organized at a larger scale. Before we spread PideYa across the network (that comes in the next two lessons), we need to zoom out one level inside the monolith itself: how objects are organized into layers, how the domain connects to the outside world, and how data access is structured. You will see that architectural patterns do not replace the GoF: they contain it. Every pattern in this lesson is, at heart, a GoF intent (or a SOLID principle) applied to the overall structure of the application.

Contents

  1. Layered architecture: where each of PideYa's GoF patterns lives
  2. MVC and its variants (MVP, MVVM)
  3. Hexagonal architecture: ports and adapters
  4. Dependency injection and IoC containers
  5. Repository and Unit of Work
  6. CQRS and Event Sourcing: a conceptual introduction

Layered architecture

The most veteran architectural pattern (it already appears in POSA, which we met in lesson 01-02) organizes code into horizontal layers, where each layer only knows the one below it:

Layer Responsibility PideYa GoF patterns that live in it
Presentation UI, web controllers, REST API Command (panel with undo), Observer (refreshing the order view)
Application Use cases, orchestration Facade (CheckoutFacade), Mediator (DispatchCenter), Template Method (reports)
Domain Business rules, entities State (OrderState), Strategy (delivery), Builder (Order.Builder), Composite (menu), Chain (validation)
Infrastructure DB, network, external services Adapter (PayPalAdapter), Proxy (cache), Abstract Factory (SpainFactory/MexicoFactory)

Notice that the table is not a new classification: it is the map of everything we built in modules 2-4, put in its place. The golden rule is the direction of the dependencies: the domain must not import anything from infrastructure. If Order knew about RedsysGateway, a change in the gateway would force a recompile of the heart of the business.

flowchart TD
    P[Presentation] --> A[Application]
    A --> D[Domain]
    A --> I[Infrastructure]
    I -.implements interfaces from.-> D

The dotted arrow is the key, and it is pure dependency inversion (DIP), which we saw in 01-03: infrastructure implements interfaces defined by the domain, not the other way around.

MVC and its variants

Within the presentation layer, the dominant pattern is MVC (Model-View-Controller):

  • Model: the state and the rules (our domain: Order, OrderState...).
  • View: what the user sees (HTML templates, the courier app's screen).
  • Controller: receives the user's action, invokes the model, and picks the view.

MVC is, essentially, Observer + Strategy + Composite working together: the view observes the model, the controller is an input-handling strategy, and views are composed of subviews. That is no coincidence: the GoF cites MVC as an example in its introduction.

Its variants adjust who knows whom — we mention them without going deeper:

Variant Key difference Where you see it
MVP The Presenter talks to the view through an interface; the view is passive Desktop UIs, classic Android
MVVM The ViewModel exposes observable state and the view hooks up via data binding Reactive frontends, Jetpack Compose, JS frameworks

Hexagonal architecture: ports and adapters

Hexagonal architecture (Alistair Cockburn) takes the layering idea to its logical extreme: the domain sits at the center, and everything else (web, DB, gateways, queues) connects through ports and adapters.

  • A port is an interface defined by the domain: it is DIP turned into architecture.
  • An adapter is a concrete implementation of that port: it is, quite literally, the Adapter pattern from 03-02 promoted to an architectural building block.

In PideYa we already had the perfect example without realizing it:

// PORT (lives in the domain): the business defines WHAT it needs
public interface PaymentGateway {
    ChargeResult charge(Order order, CardDetails card);
}

// ADAPTERS (live in infrastructure): HOW it gets done
public class RedsysAdapter implements PaymentGateway { /* Redsys API */ }
public class PayPalAdapter implements PaymentGateway { /* the one from 03-02 */ }
public class InMemoryGateway implements PaymentGateway { /* for tests */ }

PideYa's domain charges orders without knowing whether Redsys, PayPal, or a test double sits behind it. Two kinds of ports are distinguished:

  • Primary (driving) ports: where requests come in (the interface that CheckoutFacade implements — our Facade was already a primary port avant la lettre).
  • Secondary (driven) ports: what the domain needs from the outside (PaymentGateway, OrderRepository, Notifier).

Dependency injection and IoC containers

If the domain only knows interfaces, someone has to decide which concrete implementation gets used and hand it over. That is dependency injection (DI): instead of CheckoutFacade calling new RedsysAdapter(), it receives a PaymentGateway through its constructor.

public class CheckoutFacade {
    private final PaymentGateway gateway;
    private final Notifier notifier;

    // Dependencies are INJECTED: the facade doesn't know which ones they are
    public CheckoutFacade(PaymentGateway gateway, Notifier notifier) {
        this.gateway = gateway;
        this.notifier = notifier;
    }
}

An IoC container (Spring, Guice, CDI) industrializes this: it scans the classes, decides which implementation satisfies each interface, and builds the entire object graph. Seen through the catalog:

  • The container is a generalized Abstract Factory: the SpainFactory from 02-04 created coherent families by hand; Spring does it through configuration.
  • Spring's singleton beans solve the intent of the Singleton from 02-02 without its drawbacks: a single instance, yet testable and injectable — the criticism we raised there finds its answer here.

Repository and Unit of Work

We now descend to the boundary with the database, with two patterns from Fowler's PoEAA catalog (introduced in 01-02).

Repository offers the domain an apparent collection of objects, hiding the SQL:

// Secondary port: the domain talks about orders, not tables
public interface OrderRepository {
    Optional<Order> findById(String id);
    List<Order> pendingDelivery(Zone zone);
    void save(Order order);
}

The implementation (JpaOrderRepository) lives in infrastructure. The repository combines familiar intents: it is an Adapter over persistence, it often returns results via Iterator, and its complex queries can be assembled with a Builder (that is how JPA's Criteria API works, as we saw in 05-03).

Unit of Work records every object touched during a business transaction and persists them all at once on commit. If checkout modifies the order, the stock, and the loyalty points, the Unit of Work guarantees they are written together or not at all. In Hibernate you already use it without knowing: the Session/EntityManager is a Unit of Work that does dirty checking on the loaded entities.

CQRS and Event Sourcing

We finish with two patterns that lay the groundwork for the modules ahead. Just the idea for now; deploying them for real is a distributed-systems matter.

CQRS (Command Query Responsibility Segregation) separates the write model (commands: PlaceOrder, CancelOrder) from the read model (queries: "tracking screen", "kitchen list"). In PideYa a real metric motivated it: for every write of an order there are hundreds of reads of its status. With CQRS, reads hit cheap denormalized views, while writes go through the full domain (Chain validations, OrderState...). Does the "command" half sound familiar? It is the intent of the Command pattern from 04-03: requests reified as objects.

Event Sourcing goes one step further: instead of storing the order's current state, it stores the sequence of events that produced it:

OrderCreated → LineAdded(pizza) → LineAdded(soda) → OrderConfirmed → OrderCharged

The current state is rebuilt by replaying the events. Recognize the GoF intents:

  • Like Memento (04-07), it captures the past so you can return to it — but storing deltas (events) instead of full snapshots.
  • Like Observer (04-08), every persisted event can notify other interested parties: CQRS read views stay up to date by listening to those events.

That CQRS + Event Sourcing pairing is the natural gateway to the event-driven architectures we will meet in 06-03.

Common Mistakes and Tips

  • Fake layers: having controller/service/repository packages while the domain imports infrastructure classes. Layers are measured by dependencies, not by package names. Tip: use ArchUnit to verify in a test that domain does not import infrastructure.
  • Ceremonial hexagonal: creating a port + adapter for everything, including logic that will never have a second implementation. It is the patternitis of 05-05 at architectural scale. Create the port when there is a real boundary (something external, or something you need to swap out in tests).
  • Repository that leaks the technology: methods like runHql(String) on the repository interface destroy its purpose. If the domain can see HQL, there is no abstraction.
  • Adopting CQRS/Event Sourcing because it's trendy: they are expensive patterns (two models, deferred consistency, event versioning). Apply the "wait until it hurts" rule from 05-01: if a single CRUD model serves you, keep it.
  • Confusing DI with having a framework: dependency injection is passing collaborators through the constructor; you can (and should be able to) do it by hand in a main. The container merely automates the assembly.

Exercises

  1. Sort into layers. Place each PideYa class in its layer (presentation, application, domain, or infrastructure) and justify it: OrderState, PayPalAdapter, CheckoutFacade, OrderRestController, JpaOrderRepository, Order.Builder.
  2. Design a port. PideYa needs to compute estimated delivery times by querying an external traffic service. Define the port (domain interface), a real adapter, and a fake adapter for tests. State whether it is a primary or a secondary port.
  3. Events of an order. Write the sequence of events (Event Sourcing) left behind by an order that is created with two dishes, confirmed, charged, and whose customer changes the address before delivery. How would you rebuild the order's current address?

Solutions

  1. OrderState → domain (life-cycle rules). PayPalAdapter → infrastructure (implements the PaymentGateway port against an external API). CheckoutFacade → application (orchestrates a complete use case). OrderRestController → presentation (translates HTTP into application calls). JpaOrderRepository → infrastructure (implements the OrderRepository port with JPA). Order.Builder → domain (builds the aggregate with its invariants).
  2. A secondary port (the domain needs something from the outside): public interface DeliveryEstimator { Duration estimate(Address origin, Address destination); }. Real adapter: GoogleMapsEstimator implements DeliveryEstimator, which calls the API and translates its response. Test adapter: FixedEstimator implements DeliveryEstimator, which always returns Duration.ofMinutes(30). The domain computes delivery promises without ever touching the network.
  3. OrderCreated(id, customer) → LineAdded(dish1) → LineAdded(dish2) → OrderConfirmed(initialAddress) → OrderCharged(amount) → AddressChanged(newAddress). The current address is obtained by replaying the events in order: the last relevant write (AddressChanged) wins. Note the bonus: the history preserves the fact that there was an address change, information a current-state model would have lost.

Conclusion

We have zoomed out and discovered that PideYa's architecture is woven from the very threads of the catalog: layers dictate where each GoF pattern lives, hexagonal turns DIP and Adapter into the building's blueprint, IoC containers industrialize the factories, Repository and Unit of Work discipline persistence, and CQRS/Event Sourcing reinterpret Command, Memento, and Observer over the flow of data. All of this, still, inside a single deployable process. But PideYa keeps growing: the delivery team wants to deploy without waiting for the payments team, and the monolith is becoming the bottleneck. Time to split it — with its own patterns and its own dangers — in Design Patterns in Microservices.

© Copyright 2026. All rights reserved