Understanding Software Architecture
Mastering complexity rather than enduring it
Any software project of a certain scale is confronted with complexity — arising from many sources: business rules, interactions between modules, performance, cross-cutting concerns (security, logging)...
This complexity is often endured, because it tends to be handled at the code level — drowning business logic within technical constraints. The developer must juggle all of it at once: couplings multiply and responsibilities become diluted, until the breaking point where modifying one part of the code risks breaking another.
The trick is to offload some of it to other levels of abstraction — which is precisely one of the roles of software architecture, allowing us in a sense to transform algorithmic complexity into architectural complexity.
Take dependency injection as an example:
Without a container, wiring an application means manually managing the instantiation tree — who creates what, in what order, with which parameters... With a container, you simply declare that a class needs another; the container resolves the rest. Application code only sees an injected dependency.
Complexity is thus handled by specialized components, allowing the developer to focus on what matters most.
Software architecture also provides a structuring framework for organizing code cleanly: clearly defining responsibilities, boundaries, and communication interfaces between modules.
Robert C. Martin also offers a very telling definition [1]:
The primary purpose of architecture is indeed to support the life cycle of the system. Good architecture makes the system easy to understand, easy to develop, easy to maintain, and easy to deploy. The ultimate goal is to minimize the lifetime cost of the system and to maximize programmer productivity.
Fundamental Principles
Cohesion
Robert C. Martin's Common Closure Principle puts it simply: components that change together stay together. In practice, this means organizing code by functional domain rather than by technical type.
A project split into
/Services,/Helpers,/Utilsfolders may appear ordered on the surface — but these categories group classes that share nothing except their type and quickly become unmanageable in large projects.
Conversely, a module centered on a specific domain (Payment, Catalog, Authentication...) groups what belongs together: it has only one reason to change, and that reason is clear.
Minimizing Dependencies
Every dependency between modules is a point of fragility or complication when changes occur. The goal is therefore to minimize them as much as possible or to make them explicit through well-defined interfaces when unavoidable. Cyclic dependencies — A depends on B which depends on A — are particularly harmful ("Acyclic Dependencies Principle").
Encapsulation also plays a key role: exposing only what is necessary mechanically reduces the contact surface between modules, and therefore the risks of inadvertent coupling.
Dependency Inversion
Among the principles that most shape an architecture, dependency inversion is arguably the most impactful: depend on abstractions, never on implementations.
In practice, a business module must not depend directly on a database, an external service, or a framework. It exposes an interface; the infrastructure implements it. The benefit is significant: implementations can change at any time without having to modify the business code.
Layered Structure
Layered architecture is the direct application of the principles above. Robert C. Martin formalizes it as follows: dependencies may only cross boundaries in one direction, that is, from technical code toward business rules:
Monolith or Microservices?
Since the mid-2010s, microservices have been widely presented as the modern architecture par excellence — scalable, independent, continuously deployable... The reality is more nuanced: this approach introduces considerable operational complexity, often underestimated at the time of the decision.
Indeed, breaking an application down into dozens of independent services means managing as many deployments, API contracts to version, databases to synchronize, and data consistency to ensure...
Worse still: if services remain tightly coupled despite their physical separation, the result is the "distributed monolith" [2] — with all the drawbacks of both approaches and none of the benefits.
Microservices certainly have their place, but in specific contexts: significantly different scaling needs across modules, or clearly delineated business domains carried by truly independent, dedicated teams.
It is not the natural starting point for a project.
The "well-structured monolith" is far more so — and often the most rational choice, particularly when business boundaries are not yet clearly established. Martin Fowler states it clearly in his Monolith First article [3]: starting with a monolith before considering the externalization of certain modules allows you to understand the business domain before drawing boundaries between services — boundaries that, once established, are costly to revisit.
The rule of thumb: start monolithic and only extract a service when the need is proven by usage — a differentiated scaling need, a dedicated team, a clearly isolated domain... and never by anticipation.
But a monolith is not necessarily synonymous with architectural chaos: structured into well-defined modules by functional domain — sometimes called a Modulith [4] — it can offer the same organizational benefits as a microservices architecture, without the distributed complexity (which addresses very specific problems).
Hexagonal Architecture and DDD
Two complementary approaches have gradually emerged for properly structuring these "well-structured" monoliths: Hexagonal Architecture, which defines how to isolate the domain from the infrastructure, and the DDD (Domain-Driven Design) approach, which places the business domain at the heart of the project.
Hexagonal Architecture
Proposed by Alistair Cockburn in 2005, Hexagonal Architecture — also known as Ports & Adapters — concretely applies dependency inversion at the application level by defining two distinct zones: the inside (business logic), which exposes ports (interfaces), and the outside (database, user interface, external APIs...), which connects to them via adapters (interface implementations).
Domain-Driven Design
Domain-Driven Design (Eric Evans, 2003) is above all a design philosophy that places the business domain at the center of all project decisions.
It first defines a collaborative approach — building a shared language between developers and domain experts, organizing teams around functional domains — as well as a set of architectural concepts to faithfully translate that domain into code.
As with Hexagonal Architecture — business rules are isolated at the center and everything else depends on them, never the other way around. Interpretations vary but all lead to an architecture of 3 or 4 distinct layers:
- Domain (business logic)
- Infrastructure (technical implementation of interfaces)
- Application* (use case orchestration)
- Presentation (UI, controllers, CLI commands, consumers...)
* The application layer is sometimes merged into the Domain, sometimes into the Presentation layer, or occasionally split between the two.
DDD is also frequently associated with the CQRS pattern (Command Query Responsibility Segregation), which explicitly separates write and read operations.
These Tools Combine
Hexagonal Architecture, Clean Architecture, DDD, and CQRS are not mutually exclusive — they address different concerns and complement each other naturally. Herberto Graça's Explicit Architecture article [5] shows how to assemble them effectively into a coherent whole.
If software architecture is a topic that interests you, I invite you to read the very insightful series The Software Architecture Chronicles by the same author (2017), which traces the evolution of software architectures from procedural programming to modern architectural styles.
My Experience
I use DDD on a daily basis and I recommend it for the vast majority of projects — including smaller ones. Contrary to what one might think, it does not necessarily add complexity or development time, as it is first and foremost a way of organizing and naming things, and it quickly pays off from the first evolutions of the project.
POCs are probably the only exceptions, given their intentionally limited lifespan and scope.
One should not be dogmatic about the rules DDD proposes — they are not commandments, and one must keep in mind the primary goal of these principles: facilitating maintenance and maximizing programmer productivity.
Two concrete examples:
- Using Value Objects from an external library — integrating them into the Domain may seem to violate the independence rule. But in practice, the cost of eventually replacing them remains minimal (copy the class, adapt it, do a global find/replace of the FQCN) compared to the cost of unnecessarily developing and maintaining additional classes.
- Integrating Doctrine mapping directly into Entities — the rule would have it moved from the Domain to the Infrastructure layer (XML, YAML). But in practice, keeping a mapping file in sync with its entity is cumbersome and error-prone. PHP attributes directly on the entity are therefore a trade-off I fully embrace, given their equally marginal replacement cost.
References
-
[1]
📔 Robert C. Martin — Clean Architecture (2017) — the reference book on the principles of clean and maintainable software architecture
- [2] Mehmet Ozkaya — Microservices Antipattern: The Distributed Monolith (2024) — causes and consequences of this anti-pattern, with an alternative: the modular monolith
- [3] Martin Fowler — Monolith First (2015) — why to start with a monolith before considering microservices
- [4] David Heinemeier Hansson — The Majestic Monolith (2016) — a case for the "well-structured" monolith
- [5] Herberto Graça — Explicit Architecture (2017) — how to assemble DDD, Hexagonal Architecture, Clean Architecture and CQRS into a coherent whole
Further Reading
- 📔 Eric Evans — Domain-Driven Design (2003) — the founding work on DDD that established the foundations of domain-centric design
- 📔 Sam Newman — Monolith to Microservices (2019) — progressively migrating a monolith to microservices, via the modular monolith
- 📔 Martin Fowler — Patterns of Enterprise Application Architecture (2002) — a catalog of architecture patterns for enterprise applications, many of which have become today's standards: Repository, Data Mapper, Service Layer...