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
- Layered architecture: where each of PideYa's GoF patterns lives
- MVC and its variants (MVP, MVVM)
- Hexagonal architecture: ports and adapters
- Dependency injection and IoC containers
- Repository and Unit of Work
- 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
CheckoutFacadeimplements — 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
SpainFactoryfrom 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:
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/repositorypackages while the domain imports infrastructure classes. Layers are measured by dependencies, not by package names. Tip: use ArchUnit to verify in a test thatdomaindoes not importinfrastructure. - 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
- 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. - 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.
- 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
OrderState→ domain (life-cycle rules).PayPalAdapter→ infrastructure (implements thePaymentGatewayport against an external API).CheckoutFacade→ application (orchestrates a complete use case).OrderRestController→ presentation (translates HTTP into application calls).JpaOrderRepository→ infrastructure (implements theOrderRepositoryport with JPA).Order.Builder→ domain (builds the aggregate with its invariants).- 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 returnsDuration.ofMinutes(30). The domain computes delivery promises without ever touching the network. 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.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
