Capa do artigo: MVC and Hexagonal Architecture: Foundations for Software Architects

MVC and Hexagonal Architecture: Foundations for Software Architects

Software architecture permeates daily technical discussions, yet we rarely define its foundations with precision. This article explores the crucial distinction between architectural patterns and styles, examining MVC as a well-established pattern and Hexagonal Architecture as a flexible style, while also providing guidance on when and how to apply them in real-world contexts.

Before diving into the specifics, let's establish what software architecture actually is.

According to ISO/IEC/IEEE 42010:2022, software architecture is:

"The fundamental structure of a software system, which defines its components, their relationships, and the principles that guide its design and evolution."

In other words, architecture is the organizational foundation upon which the entire system is built. It represents a set of critical decisions that define the long-term structure and behavior of the application.

As Martin Fowler puts it:

Architecture is about the important stuff. Whatever that is. Important enough that it's hard to change. β€” Martin Fowler

Personally, I like to think of architecture as high-level design, present across the various layers of a system. Grady Booch expressed something similar:

All architecture is design, but not all design is architecture.

Architecture transcends specific implementation. It is about structural decisions that shape the evolution and maintainability of a system over time.

"If you think good architecture is expensive, try bad architecture." β€” Brian Foote and Joseph Yoder


Distinguishing Styles from Architectural Patterns

Architectural styles

These are general abstractions that define families of systems with common structural characteristics. They establish organizational principles without prescribing practical details. In essence, they are a way to classify architectural solutions based on shared structure and behavior.

Architectural patterns

These are more prescriptive in nature, offering structured and replicable solutions to recurring problems in software system design. As the name suggests, they are proven implementation blueprints.


The MVC Pattern: Evolution and Context

As with much of computing, nothing is as new as it seems. The MVC pattern emerged in 1970, created by Trygve Reenskaug at Xerox PARC while programming in Smalltalk. The goal was to organize graphical interfaces by separating responsibilities that had previously been chaotically mixed together in the same file.

In the 1990s and 2000s, MVC was widely adopted by frameworks such as Ruby on Rails, Struts, and Spring MVC, establishing the fundamental triad:

  • Model: handles data and business rules
  • View: displays data to the user
  • Controller: interprets user actions and coordinates the interaction between Model and View

Its widespread adoption spawned variations like MVP (Model View Presenter), MVVM (Model View ViewModel), and HMVC (Hierarchical MVC), each adjusting responsibilities to suit specific contexts.


Integration with Layered Architecture

In 2003, Martin Fowler's book "Patterns of Enterprise Application Architecture" introduced the Passive View pattern, which directly influenced MVP. In the same work, he described patterns such as Layered Architecture, Domain Model, Service Layer, and Data Mapper, all of which shaped modern enterprise architectures.

This evolution led to MVC being integrated with N-Tier architecture, refining the separation of responsibilities and decoupling business rules from the presentation layer. This made it easier to swap out View-focused frameworks, reducing the impact of technology changes.

This is how the pattern we know today came about: Controller, Service, and Repository. In this scenario, the MVC pattern became focused exclusively on the presentation layer, while the application's domain was implemented according to other approaches, often influenced by DDD.


MVC in the Modern Ecosystem

Spring MVC is a great example of the evolution of pattern-oriented architecture. With the introduction of annotations in Java, the framework became significantly more concise and readable compared to other solutions based on XML or verbose configurations.

In 2009, the @ResponseBody annotation was introduced, allowing controllers to return data directly (in JSON, XML, or other formats), eliminating the need to render HTML. This was a major milestone for API development.

Then in 2014, the @RestController annotation consolidated this behavior by combining @Controller with @ResponseBody, further simplifying the creation of RESTful endpoints.

With the rise of microservice-based architectures, the REST style gained widespread adoption, mainly due to its simplicity, flexibility, and low technology barrier. REST became the de facto standard for service-to-service communication, especially in distributed environments.

A point that frequently causes confusion is the distinction between the MVC pattern and the three-tier (N-Tier) architecture. The diagram below illustrates how these concepts relate to each other and how they are applied in practice using Spring MVC as an example.

Spring implements the MVC pattern to structure the application's entry point (mainly in web applications), while simultaneously encouraging a clear separation between Controller, Service, and Repository, forming a typical layered architecture.

This arrangement is flexible: controllers can return different data formats, such as JSON, XML, or others, depending on the type of response needed.

However, returning JSON does not necessarily mean we are using REST, just as using XML does not automatically mean we are dealing with SOAP. These are merely data representation formats. The protocol or architectural style in use goes beyond that choice. In short, not every endpoint that returns JSON is REST. Not every XML response is SOAP. These are distinct concepts that deserve a deeper discussion, a topic for another article.


The Hexagonal Style: Architectural Flexibility

In 2005, Alistair Cockburn published the concept of Hexagonal Architecture, also known as Ports and Adapters, in the technical report Hat β€” Technical Report 2005 (HaT β€” Human and Technology), developed by Alistair's own consultancy.

As an architectural style, the hexagonal approach provides guiding principles without prescribing specific implementations. The concept brings together: domain isolation, separation of responsibilities, and technology independence, with emphasis on inversion of control and dependency inversion.

The Dependency Inversion Principle (DIP), one of the SOLID principles, states that high-level modules should not depend on low-level modules. Both should depend on abstractions, typically through interfaces. The concrete implementations of those interfaces are injected via Inversion of Control (IoC), enabling flexibility, testability, and greater decoupling between layers.


How Hexagonal Architecture Works

The mechanism is elegant: all interaction with the domain, or from the domain to the outside world, happens through ports. In practical terms, in languages like Java or C#, these ports are implemented as interfaces. The concrete implementations of these interfaces are called adapters.

An important point to highlight is that ports are part of the domain. They serve as the interface for interacting with it. Furthermore, this structure allows testing the domain in isolation, since the adapter implementations live outside the domain. This ensures the domain remains isolated from external technical implementations.

Another fundamental aspect to understand is that the application of ports and adapters is not limited to just "swapping databases." Ports and adapters are a way to structure the application's interaction with the outside world. For example, if the application previously had only a single entry point via a REST API, and now it needs to integrate with another system through messaging or streams, you only need to implement a new adapter and connect it to your architecture using the same "input port." This shows that the approach is not merely about replacing one implementation with another. That would be an oversimplification of what this concept truly offers.

This approach offers full interchangeability, allowing total implementation flexibility. It is not just about "swapping databases," but about enabling fine-grained adjustments across various aspects of the architecture with little to no changes to the domain. Practical examples of this flexibility include:

  • Removing the ORM and using plain JDBC for a specific performance optimization.
  • Implementing a cache layer without impacting the core domain.
  • Applying feature toggles for canary deployments or running A/B tests.
  • Customizing implementations for specific contexts, such as new integration types or changing performance requirements.

An important aspect to highlight is that Hexagonal Architecture, being a style, is not prescriptive about the specific implementation of ports and adapters. It does not dictate how you should organize your code, which directories, packages, or class names to use. The focus is on applying the principles and fundamentals of good practices, such as separation of responsibilities and domain isolation. For example, in a language like Python, which does not natively support interfaces, you can implement ports using protocols or abstract classes to define contracts, achieving the same flexibility for interacting with the domain. This reinforces the power of the model: it adapts to the nature of the project and the technologies in use, allowing the architectural principles to be followed regardless of the language or framework.


Why Hexagonal?

The choice of the name hexagonal for this architecture is not just an aesthetic decision but also a functional one. Alistair Cockburn, when introducing Hexagonal Architecture in his technical report "Hat β€” Technical Report 2005", explained that the hexagonal shape was chosen because it provides a clear and efficient visual space for representing multiple ports and adapters connected to the domain. This shape overcomes the limitations of traditional layered diagrams, allowing for a more flexible and less rigid representation of the system's interactions.

Beyond its visual purpose, the name hexagonal also became an effective marketing element. Its shape and association with the concept help make the model memorable and distinctive, making it easier to adopt and spread.


The Danger of Reducing Concepts to Simplistic Solutions

In software development, a common tendency is to apply widely accepted concepts without considering the specifics of the problem. This frequently leads to the oversimplification of concepts like coupling and duplication, creating solutions that, instead of simplifying the system, make it more complex and harder to maintain.


When Over-Decoupling Creates Problems

While decoupling is considered a best practice for improving a system's flexibility and testability, applying the idea that "all coupling is bad" too rigidly can be harmful. In some cases, a controlled dependency between components is not only inevitable but necessary to ensure cohesion and the proper functioning of the system.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand. The more a program is designed to be read, the easier it will be to maintain." β€” Martin Fowler, Refactoring: Improving the Design of Existing Code

For example, excessively decoupling components can result in a fragmented system where each part requires complex interfaces to communicate. This increases code complexity and the learning curve for developers, who need to understand how all the parts connect. Instead of pursuing decoupling at all costs, the more effective practice is to evaluate what level of coupling is necessary to ensure cohesive interaction between components.


Duplication: When It Can Be the Best Solution

Similarly, the idea that all duplication is bad is an oversimplification. While large-scale code duplication is an anti-pattern, there are scenarios where controlled duplication can be beneficial. In some cases, duplicating a small portion of code can be simpler and more efficient than creating a complex abstraction that often makes the code harder to understand and maintain.

For example, if there is a localized change that needs to be made in several parts of the system, it may be more practical to duplicate the code rather than refactoring the entire system toward a centralized approach. In this case, duplication can improve readability and maintainability, as well as reduce the risk of introducing complex bugs when modifying a central part of the system. This pragmatic approach focuses on solving immediate problems efficiently, without chasing an abstract ideal.

"Duplication is not, in itself, evil. The problem is when duplication is unnecessary. Instead of avoiding duplication at all costs, we should seek a balance: eliminate duplication when it creates complexity or inconsistency, but allow controlled duplication where it facilitates maintenance or code readability." β€” Martin Fowler, Refactoring: Improving the Design of Existing Code

The key is not to follow best practices blindly, but to adapt them to the system's context. Decoupling is important, but not to the point of making the system fragmented and hard to understand. Duplication can be a problem when it is unnecessary, but in some situations, it can be a practical and efficient solution, making the code more readable and easier to maintain. In both cases, the key is to evaluate the specific needs of the project and apply best practices with pragmatism.

As Martin Fowler correctly points out, software design should not be a blind pursuit of abstract theories, but rather a balanced application of principles, adjusted to fit the requirements and context of the system. The flexibility to adapt concepts to the real world is what makes good software design stand out.

The understanding and application of these practices should not be dogmatic. Every architectural decision should be driven by the need to solve a real problem, not by the mere application of a theoretical principle. By focusing on the system's needs and on maintainability and readability, we can create solutions that, far from oversimplifying, add value and clarity over time.

"The art of programming is the art of organizing complexity, of mastering multitude and avoiding its bastard chaos as effectively as possible." β€” Edsger Dijkstra


Conclusion

MVC, layered architecture, and Hexagonal are not rivals in an architectural ring. They are complementary tools that solve problems in different dimensions. MVC organizes presentation, layers structure responsibilities, and Hexagonal isolates the domain. Mastery lies in recognizing when each one best serves the system.

The real insight for experienced architects is understanding that there is no perfect architecture, only adequate architecture. Every decision carries trade-offs that will echo throughout the system's lifetime. What separates a mature architect is not knowledge of patterns, but the wisdom to apply them with pragmatism.

As Dijkstra reminds us, programming is "the art of organizing complexity." In a world where complexity grows exponentially, our responsibility as architects is not to eliminate it. It is to make good choices. We do this not by following dogma, but by building systems that humans can understand, modify, and evolve.

Software architecture is, ultimately, a conversation between the present and the future. Every line of code we write today is a letter to tomorrow's developers. May that letter be clear, respectful, and above all, useful.

Frameworks change, technologies become obsolete, requirements evolve. What survives are well-founded structural decisions and the ease of change we build into the system.

The code we write today is the legacy we leave for ourselves and those who come after us.