Skip to content

6.2 Application Architecture

Milancho Arsovski edited this page Mar 9, 2023 · 26 revisions

Introduction

Common design principles

Separation of concerns

Architecturally, applications can be logically built to follow this principle by separating core business behavior from infrastructure and user-interface logic

// example

Encapsulation (10)

Different parts of an application should use encapsulation to insulate them from other parts of the application. Application components and layers should be able to adjust their internal implementation without breaking their collaborators as long as external contracts are not violated. A key consideration in the domain-driven design and clean architecture is how to encapsulate access to data, and how to ensure the application state is not made invalid by direct access to its persistence format.

// example

Dependency inversion (10)

The direction of dependency within the application should be in the direction of abstraction, not implementation details. That is, if class A calls a method of class B and class B calls a method of class C, then at compile time class A will depend on class B, and class B will depend on class C. Applying the dependency inversion principle allows A to call methods on an abstraction that B implements, making it possible for A to call B at run time, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). Dependency inversion is a key part of building loosely coupled applications since implementation details can be written to depend on and implement higher-level abstractions.

Explicit dependencies (13)

Methods and classes should explicitly require any collaborating objects they need in order to function correctly. Following the principle makes your code more self-documenting and your coding contracts more user-friendly.

It states that objects should have only one responsibility and that they should have only one reason to change. Adding new classes is always safer than changing existing classes since no code yet depends on the new classes. In a monolithic application, we can apply the single responsibility principle at a high level to the layers in the application.

The application should avoid specifying behavior related to a particular concept in multiple places as this practice is a frequent source of errors.

Persistence ignorance (PI) refers to types that need to be persisted, but whose code is unaffected by the choice of persistence technology. Such types in .NET are sometimes referred to as Plain Old CLR Objects (POCOs), because they do not need to inherit from a particular base class or implement a particular interface. 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.

Bounded contexts are a central pattern in Domain-Driven Design. They provide a way of tackling complexity in large applications or organizations by breaking it up into separate conceptual modules. At a minimum, individual web applications should strive to be their own bounded context, with their own persistence store for their business model. Communication between bounded contexts occurs through programmatic interfaces, rather than through a shared database.

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

What is a monolithic application?

A monolithic application is one that is entirely self-contained, in terms of its behavior (runs within its own process). If such an application needs to scale horizontally, typically the entire application is duplicated across multiple servers or virtual machines.

All-in-one applications

The smallest possible number of projects for an application architecture is one. Separation of concerns is achieved through the use of folders.

A single project ASP.NET Core app (VS Solution Structure)

  • Data Access Logic
    • EF Migrations
    • EF DbContext and model design
  • UI Models
    • AccountViewModels
  • Application Services (Interfaces and Implementations)
    • IEmailSender
    • ISmsSender
    • MessageService
  • Presentation Logic
    • Views
      • Account
  • Application
    • appsettings.json
    • program.cs

What are layers?

As applications grow in complexity, one way to manage that complexity is to break up the application according to its responsibilities or concerns. By organizing code into layers, common low-level functionality can be reused throughout the application. In this way, each layer has its own well-known responsibility. Breaking the application into three projects by responsibility (or layer).

Traditional "N-Layer" architecture applications

  • Application Layers
    • User Interfaces (UI)
    • Business Logic (BLL)
    • Data Access (DAL)

Clean architecture

  • Dependency Inversion Principle
  • Domain-Driven Design (DDD) principles
  • Clean Architecture (aka Onion, Hexagonal, Ports-and-Adapters) organizes your code in a way that limits its dependencies on infrastructure concerns.
  • Clean architecture puts the business logic and application model at the center of the application.
  • Two Approaches to Architectural Layers

    • N-Tier / N-Layer
    • Clean Architecture (Onion, Hexagonal, Ports, and Adapters)
  • Clean Architectural Rules

    • Model all business rules and entities in the Core project
    • All dependencies flow toward the Core project
    • Inner projects define interfaces; outer projects implement them
  • What belongs in the Core project

    Application Core Project
    Interfaces Domain Events / Event Handlers Aggregates
    POCO Entities Domain/Application Exceptions Specifications
    Domain/Business Services Value Objects Custom Guards
    Enums Validators
  • What belongs in the Infrastructure project

    Infrastructure project
    Repositories EF Core DBContext
    DbContext classes
    InMemory Data Cache
    (Cached Repositories)
    Other Web API Clients Redis Cache Service
    File System
    Accessors
    Azure Storage
    Accessors
    Azure Service Bus Accessor
    Emailling
    Implementations (SendGrid, etc)
    SMS Service
    Implementations (Twillo)
  • What belongs in the Web project

    Web project
    API Endpoints Razor Pages Controllers Views ASPNET Core Identity
    API Models ViewModels
    Filters Response Caching Filter Model Validation Filter
    Model Binders Composition Root
    Tag Helpers Other Services Interfaces
  • What belongs in the Shared Kernel project

    Shared project
    Base Entity Base Value
    Object
    Base Domain
    Event
    Base
    Specification
    Common
    Interfaces
    Common
    Exceptions
    Common Auth
    Common
    Guards
    Common
    Libraries
    DI Logging Validators
  • Dependencies flow

    • ASP.NET Core Web APP -> Infrastructure Project (compile-time dependencies)
    • ASP.NET Core Web APP ..> Infrastructure Project (runtime-only dependency)
    • ASP.NET Core Web APP -> Application Core Project (compile-time dependencies)
    • Infrastructure Project -> Data Source (compile-time dependencies)
    • Infrastructure Project -> Third Party Services (compile-time dependencies)
    • Infrastructure Project ..> Application Core Project (runtime-only dependency)

Technologies

  • Modular Monolith ✅
  • Railway-Oriented Programming ✅
  • Marten ✅
  • Sagas
  • gRPC
  • Event-Driven Architecture

Clean Architecture

eShopOnWeb

Shopify Engineering

Additional resources

Notes