DEV Community

Cover image for Clean Architecture for Enterprise Applications: A Practical Guide from the Trenches
Rayson Lawrence Dsouza
Rayson Lawrence Dsouza

Posted on • Edited on

Clean Architecture for Enterprise Applications: A Practical Guide from the Trenches

Table of Contents

Introduction

Have you ever faced a situation where you wanted to separate concerns like logging, database operations, and business logic?

While this can be partially achieved by tweaking a traditional monolithic architecture, there's a better way.

What if I told you there's an architectural style designed to separate these concerns cleanly and effectively?

Yes — that’s Clean Architecture. Let’s dive in and explore why it works so well.


Clean Architecture in Nutshell

Clean Architecture is a layered architecture that enforces separation of concerns through strict boundaries. Each layer has a clear role and direction of dependency.

Typically, There are four Main Clean Architecture Layers:

Clean Architecture Model

Domain:

This is the core and innermost layer of the architecture. It has no dependencies on other layers and contains the pure business model of your application. You won't find any NuGet packages here.

It typically includes:

  • Enums
  • Models
  • Constants
  • Value Objects

Application:

The Application layer depends only on the Domain layer. It contains use cases and business logic that orchestrate operations using domain entities.

Here you define:

  • Application services (use cases)
  • Interfaces for repositories or external services
  • Validation rules and constraints

Example: Logic for how a request should be created, or when a workflow should be triggered.

Infrastructure:

This layer provides implementations for interfaces defined in the Application layer. It depends on the Application. But, As the Application depends on Domain layer. Infrastructure layer also depends on Domain layer.

Typical components:

  • Database context (e.g., AppDbContext in EF Core)
  • Repository implementations
  • External services (e.g., mail, file storage)
  • Data migrations and configurations

Presentation Layer:

The outermost layer of the system. It provides the user interface or API that external clients interact with. It depends on the Application and Infrastructure layers.

It usually contains:

  • API endpoints (e.g., Minimal APIs or Controllers)
  • Middleware (e.g., global exception handling)
  • Authentication & Authorization logic

The Question: Why?

I used to wonder whether Clean Architecture was the right fit for our use case — modernizing a suite of homegrown applications.

The company I currently work for maintains over 20 internal applications, each built over time with different frameworks and architectural styles. Some were in Angular, others in .NET, and a few in React. As the system grew, maintaining expertise across so many tech stacks became difficult and inefficient. We didn’t want to depend on hiring developers with specific skills for each application.

So, we made a strategic decision: standardize all new and modernized apps using a single stack — .NET with Clean Architecture. It offered better scalability, clear separation of concerns, and most importantly, consistency across all applications.

Initially, this approach felt like over-engineering — especially for our smaller apps. Clean Architecture introduces more layers and structure, which might feel heavy when your app has just a few features or a limited user base (ours was around 2,000 users). But let's be honest,

Architecture isn't only about scale of users — it's about scale of complexity.

Even if an app starts small, business logic can grow rapidly. With monolithic structures, you risk dumping everything into one massive "business layer" over time. Clean Architecture helps you avoid that by enforcing boundaries from the beginning.

Yes, it’s more complex upfront. New developers might need time to get used to the structure. But once they do, they’ll appreciate how easy it is to navigate, maintain, and extend the codebase — layer by layer, feature by feature.


The Question, How?

How do you actually build a Clean Architecture solution?

The most important thing for us, when modernizing over 20 internal applications, was to establish a common boilerplate — a reusable template that could serve as a starting point for every application we build going forward.

Here’s an overview of that structure:

Overall Template Structure

In this post, I’ll explain the structure at a high level. In future posts, I’ll dive deeper into each component. Now, Let's start with creating a blank solution in VS 2022. Then follow up:


Solution Items:

These files apply settings across the entire solution, not
just individual projects

  • .editorconfig: Contains code formatting rules. For example, in Visual Studio 2022, this can enforce naming conventions (e.g., readonly variables must start with _, static variables with s_).

Create it with: Add → New Item → Editor Config (.NET)

  • Directory.Build.props: Stores common metadata for all projects (e.g., name, description, Git repo URL).

Create it with: Add → New Item → XML File

<Project>
  <PropertyGroup>
    <Title>IS Web Application Template</Title>
    <Authors>Rayson Dsouza</Authors>
    <Description>This template will be the base for all the applications that are going to be built under IS. These include API, Web Blazor Applications.</Description>
    <RepositoryType>gitlabs</RepositoryType>
    <PackageTags>api project, api endpoints, response, web ui</PackageTags>
    <RepositoryUrl>{YOUR_GITLAB_REPO_URL}</RepositoryUrl>
    <LangVersion>12</LangVersion>
    <RootNamespace>ISWebAppTemplate</RootNamespace>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>false</InvariantGlobalization>
  </PropertyGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode
  • Directory.Packages.props: Centralizes NuGet package references across the solution, avoiding redundancy.

Create it with: Add → New Item → XML File

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Carter" Version="8.2.1" />
    <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.16" />
    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4" />
    <PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
    <PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.10.0" />
  </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

src/Backend: Clean Architecture Backend

This folder holds the backend solution based on Clean
Architecture.

  • ISWebAppTemplate.Api:

Template Presentation Layer

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <RootNamespace>ISWebAppTemplate.Api</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Carter" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ISWebAppTemplate.Application\ISWebAppTemplate.Application.csproj" />
    <ProjectReference Include="..\ISWebAppTemplate.Infrastructure\ISWebAppTemplate.Infrastructure.csproj" />
  </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

The Presentation Layer (outermost layer). An ASP.NET Core
Web API project. You can refer to Presentation layer for more details.

  • ISWebAppTemplate.Application:

Template Application Layer

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <RootNamespace>ISWebAppTemplate</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\ISWebAppTemplate.Domain\ISWebAppTemplate.Domain.csproj" />
  </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

The Application Layer. A class library focused on business
logic. You can refer to Application layer for more
details.

  • ISWebAppTemplate.Infrastructure:

Template Infrastructure Layer

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <RootNamespace>ISWebAppTemplate.Infrastructure</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ISWebAppTemplate.Application\ISWebAppTemplate.Application.csproj" />
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

The Infrastructure Layer. A class library focused on Handling
database and external service interactions. You can refer to
Infrastructure layer for more details.

  • ISWebAppTemplate.Domain:

Template Domain Layer

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <RootNamespace>ISWebAppTemplate.Domain</RootNamespace>
  </PropertyGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

The Domain Layer (innermost/core layer). Contains domain
entities, value objects, and core rules. You can refer to
Domain layer for more details.


src/Frontend: Blazor Web UI

This folder contains the frontend application. I have
created Blazor Web App and selected Blazor server as
renderer.

  • ISWebAppTemplate.WebUI:

Template UI Structure

A Blazor Server application with:

  • Radzen for UI components
  • Azure Entra MFA Authentication
  • Correlation ID generation for tracing requests across downstream APIs — helpful in debugging and log analysis.

/tests: Unit and Integration Testing

We separate tests into two clear layers:

  • ISWebAppTemplate.Integration.Tests:

Template Integration Test

  • Focus: Full transaction and behavior testing
  • Example: If a requester is in Band X, no approval is
    required; otherwise, admin approval is triggered.
    Integration tests verify this business logic
    holistically.

  • ISWebAppTemplate.Unit.Tests:

Template Unit Test

  • Focus: Validations and logic at the entity level
  • Example: The request title must be at least 50 characters long

The Key Difference:
Unit tests validate isolated components like a field or
rule.
Integration tests validate end-to-end behavior and rule
application.


Conclusion

I know this has been a lot to take in — especially since I’ve only scratched the surface. But don’t worry, I’ll be diving deeper in upcoming posts. Each of the techniques and patterns used in this project will be covered individually to give you a clearer and more practical understanding of how they work in real scenarios.

So why Clean Architecture over a traditional monolith?

It all comes down to separation of concerns, scalability, and long-term maintainability. Clean Architecture gave us a structure that helps manage complex business logic across 20+ applications — something that would have become unmanageable with a typical monolithic setup.

Note: We are not using Docker or containerization in this setup. Since our applications serve around 2,000 users and are hosted on a stable on-premise server, the performance needs are easily met. The main reason for adopting Clean Architecture was to better manage complex business logic, not to meet scale or deployment flexibility.

Next Up: Create and Publish a Custom .NET Project Template and Private NuGet to GitLab Package Registry

Top comments (0)