Enterprise Application Architecture: Best Practices and Strategies
By Samir Hadzajlic, Software Developer at Authority Partners
Explore a journey to revolutionize your software development practices. Immerse yourself in the challenges of code maintainability, architectural pitfalls and the transformative power of Clean Architecture principles. Discover how a code review and refactoring session can reshape your codebase, enhancing its longevity, scalability and adaptability. Read on to explore actionable insights and strategies that will elevate your code to new heights. Ready to transform? Let’s dive into the world of optimized application architecture!
Introduction
Writing software is easy and sometimes exciting, especially when we build an application from scratch. When it comes to modifying code written by other developers or code you wrote six months ago, it can be a bit of a bore at best and a nightmare at worst. The software works, but you aren’t sure exactly how. It has been created using an Agile approach, but introducing new features into the codebase is harder than it should be. Working on such systems becomes a chore, leaving developers frustrated and devoid of any coding pleasure. So, what makes many recent applications hard to understand and maintain?
The road to hell is paved with good intentions.
Here are some main architectural mistakes that make applications hard to understand and maintain.
- Applications are often developed using the Smart UI antipattern or even using some sort of Layered, MVC or Service-oriented architectural patterns, but without distinct layers and boundaries or even if they do exist, they are overlapped and tightly coupled, and their boundaries are blurry. As a result, business logic is often spread across layers, which leads to leaking into the user interface or tightly coupled to the database, infrastructure services, or external libraries and services. If we have to consume specific logic in a different way, we have to rewrite existing business logic or at least one of its parts. Also, we are often forced to reference other application services from a single execution path. Business logic is often inflicted and mixed with infrastructure components and depends on their implementation.
- Applications are almost always developed using the so-called Anemic Domain Model. It’s a domain model without any behavior and only data properties. Anemic Domain Models work great in simple applications, but they are difficult to maintain and evolve if you have rich business logic. The important parts of your business logic and rules end up being scattered all over the application. It reduces cohesiveness and reusability, and makes adding new features more difficult. By some authorities like Eric Evans or Martin Fowler, this is an anti-pattern, although it seems to be getting more popular.
- Rushing into development without proper planning, including resource allocation, timeline estimation and risk management often result in poor application design and structure and can result in project failure. Furthermore, Agile/scrum promotes the myth that process can replace product design and architecture. The management fairytale is that great products can be reliably developed simply by obsessing over process. Software projects are at risk of failure principally because of improper technical design with insufficient resourcing. Well-prepared and trained developers are crucial in development of consistent application in both style and design. Furthermore, it is impossible to constantly shift major requirements while maintaining schedule and quality.
Foundational Design Principles
So, how can we avoid the pitfalls, obstacles and drawbacks mentioned above? To build a maintainable, testable and extensible business application, you have to follow some of the most basic design principles. These design principles are a collection of guidelines, best practices and concepts that assist developers in creating well-structured, maintainable and efficient software systems. When you follow proven design principles, your code base becomes infinitely more flexible, maintainable and adaptable to change.
Design principles increase cohesion and decrease coupling. Coupling is degree of dependency between two modules (layers, projects, classes…). We always want low coupling. Cohesion is measure by how strongly functions inside class are related. This is the measure of how strongly related is the set of functions performed by a single module. We always want high cohesion. Although there are some other principles, I will name here just the most important ones:
- Separation of Concerns (SoC) is the process of dissecting a piece of software into distinct features that encapsulate unique behavior and data that can be used by other modules. The main idea here is that software system must be decomposed into parts that overlap in functionality as little as possible. This is the most important principle in software engineering. The act of separating a program into discrete responsibilities significantly increases code reuse, maintenance and testability.
- Dependency Inversion Principle (DIP) is all about isolating your classes from concrete implementations and having them depend on abstract classes or interfaces. It promotes the mantra of coding to an interface rather than an implementation, which increases flexibility within a system by ensuring you are not tightly coupled to one implementation.
- Closely linked to the DIP are the Dependency Injection (DI) and the Inversion of Control (IoC). Dependency Injection is the act of supplying a low level or dependent class via a constructor, method or property. Used in conjunction with Dependency Injection, these dependent classes can be inverted to interfaces or abstract classes that will lead to loosely coupled systems that are highly testable and easy to change.
- Single Responsibility principle or SRP is closely aligned with SoC. It states that every object should only have one reason to change and a single focus of responsibility. By adhering to this principle, you avoid the problem of monolithic class design that is the software equivalent of a Swiss army knife. By having concise objects, you again increase the readability and maintenance of a system
- Don’t repeat yourself or DRY principle aims to avoid repetition of any part of a system by abstracting out things that are common and placing those things in a single location. This principle is not only concerned with code but any logic that is duplicated in a system; ultimately there should only be one representation for every piece of knowledge in a system.
The principle of Persistence Ignorance (PI) holds that classes modeling the business domain in a software application should not be impacted by how they might be persisted.
Application Architecture and Design
You cannot build a maintainable and scalable application on a poor foundation. Planning good architecture is critical to the success of an application. Application architecture is a set of patterns, concepts and techniques that organizations use to determine how software should be built. Layering is an important concept of application architecture and design because it helps to enforce the separation of concerns and involves organizing code and functionality into distinct layers or tiers, each responsible for a specific set of tasks. Typical layered architecture usually contains three layers:
Figure 1: Typical Layered Architecture
There are several examples of architectural styles that apply to the internal structure of a business application like Hexagonal, Service-Oriented Architecture (SOA), Microservices Architecture and Traditional Layered Architectures. They are all based on the principle of separation of concerns, which means dividing the application into distinct layers or modules that have different responsibilities and dependencies.
Clean Architecture
Recently, Clean Architecture became more popular and that is why I am going to describe this particular architecture in further text. Clean Architecture is a concept introduced by Robert C. Martin, also known as Uncle Bob. Robert C. Martin is a well-known software engineer and author, and he presented the principles of Clean Architecture in his book titled “Clean Architecture: A Craftsman’s Guide to Software Structure and Design,” which was published in 2017.
The four main layers of clean architecture consist of:
- Domain Layer: It’s the core of the architecture, where business logic gets implemented. In addition, this layer doesn’t depend on any other layers and functions per the code written using classes and interfaces.
- Application Layer: Resides in the middle of the presentation and Domain Layer and functions as an intermediate between them. Primarily, it maintains the data between corresponding layers but is independent on the infrastructure and Presentation Layer.
- Infrastructure Layer: All the file systems and databases get configured at the Infrastructure Layer. It aids the application to interact with external components to maintain the workflow and process data.
- Presentation Layer: As the name defines, the Presentation Layer is for writing the interface code. ASP.NET Core is used for building the interface. This layer is usually a single page application built with Blazer, Angular, React or some other frameworks.
Clean Architecture is heavily based on the previously mentioned design principles. It brings us a way of structuring the application so that the business logic is encapsulated at the heart of the application, making it independent from other layers and modules. One of its key features to achieve this is being very heavily based on the separation of concerns. In traditional architecture, “higher” level modules depend on “lower” level modules. The Presentation Layer is the highest layer and traditionally depends directly upon – and may communicate directly with – the business layer, while the business layer depends upon – and may communicate with – the data access layer.
In Clean Architecture, when the principle of dependency inversion is applied, this relationship moves inwards. This means that the outer layers depend on the layer immediately below them, but the inner layers do not depend on any of the outer layers that lie around them. So, the innermost layer does not depend on anything. As shown in the image below, the application core does not depend on any other layer. This layer is the backbone of the application and contains business entities, high-level abstractions (interfaces) for services that reside in outer layers and business logic that resides within domain services as well as in the entities themselves. It has no dependencies and external influences; therefore, it is completely agnostic to the external circles. It should also not depend on any specific framework (which is not always the case). For example, it should not reference the System.Net.Http or EntityFramework.Core NuGet package.
Anyway, for practical reasons, we could reference some NuGet packages like AutoMapper or System.Text.Json from the core application projects.
Figure 2: Circular Design
The Infrastructure layer in Clean Architecture is responsible for dealing with external concerns and technologies. This layer is at the outermost boundary of the application and includes frameworks, tools, libraries, databases and other infrastructure-related code. It acts as a bridge between the inner layers, which contain the core business logic and the external world.
Specifically, in the Infrastructure layer, you’ll find implementations of database access, integration with external frameworks and libraries and interaction with external I/O operations, such as file systems, network communication or any other form of input/output. It also interacts with external services, whether they are cloud-based services, payment gateways or other third-party services. Finally, it is used for reading configuration files and managing application settings, as well as the implementation of logging and monitoring tools.
One specific characteristic of the Infrastructure layer is that it should be replaceable without affecting the core business logic (Application Core). This means you can swap out a database, change a web framework or update a third-party library without requiring changes to the application’s essential functionality. The goal is to keep the core of the application (Entities and Use Cases) independent of the specific technologies used for infrastructure. This flexibility makes it easier to adapt to changes in external tools or upgrade to newer versions without disrupting the overall system architecture. This is, in my opinion, the most important feature of Clean Architecture because the core domain is the area of the application that holds the most value and is key to the success of the application. If we want to move on to a new technology, we can do that without changing anything in this most valuable part of the application. As the image below clearly shows, infrastructure projects will reference the core projects and it is through dependency inversion that the actual implementations get plugged into the core from the infrastructure at runtime dependency injection. Basically, the dependencies (references) have been inverted, thus the name of the principle.
Figure 3: Outer layers reference inner layer (core domain)
The Application Layer in Clean Architecture is the bridge between the Presentation Layer and the Domain Layer. It orchestrates the execution of use cases, transforms data and ensures the proper application of business rules. The communication between layers is achieved through well-defined interfaces, promoting a clear separation of concerns and maintainability in the overall system design. Do not confuse the Application Layer with the application core (also known as the core Domain Layer or Business Layer). For example, an Application Layer in .NET Core could be a Web API project that contains controllers through which the Presentation Layer communicates with the application Core Domain Layer. This layer is also sometimes called the Service Layer due to this ambiguity. The next image shows how this layer interacts with outer and inner layers. Though these architectures all vary somewhat in their details, they are very similar. They all have the same objective, which is the separation of concerns. They all achieve this separation by dividing the software into layers.
Figure 4: The Clean Architecture by Robert C. Martin.
In Clean Architecture, the Presentation Layer is one of the architectural layers that focuses on user interface and interaction. It is responsible for handling user input, presenting data to the user and interacting with the other layers to orchestrate the application’s flow.
The Presentation Layer communicates with the underlying layers, such as the Application Layer and the Domain Layer (which hold the core business entities and business logic). This communication is usually done through interfaces or boundaries, maintaining a clear separation of concerns.
This is all nice, but how can we distinguish and distribute these layers in .NET Core applications? We do that through folders and projects. Here is one simple example:
Figure 5: Clean Architecture example in .NET Core application.
The project names within src align closely to the layers of the Clean Architecture diagram, the only exception being API, representing the Application (or Service) Layer. In this particular example, this layer consists of a single Web API project.
The tests folder contains numerous unit and integration tests projects to help get you up and running quickly.
Aside from .NET Core, numerous technologies are used within this solution, including:
- CQRS with MediatR
- Validation with FluentValidation
- Object-Object Mapping with AutoMapper
- Data Access with Entity Framework Core
- Web API using ASP.NET Core
- UI using ASP.NET Core Blazor
- Open API with NSwag
- Security using ASP.NET Core Identity + IdentityServer
- Automated Testing with xUnit.net, Moq and Shouldly
Along these frameworks and technologies, the Mediator pattern can be used to help us achieve a high level of loose coupling by enabling messaging (communication) between different objects instead of creating tightly coupled objects.
Adopting Clean Architecture principles when building .NET Core applications is a strategic choice that pays off in terms of maintainability, flexibility and testability. By adhering to the principles of separation of concerns and dependency inversion, developers can create robust systems that are easy to extend, refactor and adapt to changing requirements or switching to a new technology. Here are the main benefits of choosing Clean Architecture for your awesome future business applications:
- Clean Architecture puts the core domain logic at the center of the design. This ensures that the business rules are isolated from external concerns, making it easier to understand and maintain.
- The architecture allows for changes in external details, such as user interfaces or databases, without impacting the core business logic. This adaptability is crucial in a dynamic software development environment where requirements evolve.
- With well-defined boundaries between layers, it becomes straightforward to write unit tests for the core business logic. Testability is not an afterthought, but rather is built into the architecture, enabling a robust and reliable test suite.
- Clean Architecture promotes independence from frameworks and external tools. This is particularly important for .NET Core applications, as it allows developers to upgrade frameworks or switch to alternative technologies without rewriting the entire application.
As you embark on building your .NET Core application using Clean Architecture, remember that architecture is not a one-size-fits-all solution. It is a set of principles and guidelines that can be adapted to the specific needs of your project. By applying these principles judiciously, you’ll be well on your way to building a sustainable, scalable and responsive .NET Core application that stands the test of time.
Ready to transform your application’s architecture? Upgrade your codebase with our expert Code Review and Refactoring session. Enhance maintainability, scalability and adaptability. Contact us now for a consultation at hello@authoritypartners.com or fill out this form to improve your code and enhance your success!
Additional Resources
Want to learn more about Clean Architecture? Check out our podcast here, where we further guide you through this interesting topic.
Moreover, you can find out more about Clean Architecture on these websites:
- The Principles of Clean Architecture by Uncle Bob Martin
- Clean Architecture – Robert (Uncle Bob) Martin
- The Clean Code Blog by Robert C. Martin (Uncle Bob)
- Clean Architecture with ASP.NET Core 3.0 – Jason Taylor – NDC Sydney 2019
- Clean Architecture with .NET Core: Getting Started