DEV Community

Cover image for Clean Architecture in C#
Omotola Odumosu
Omotola Odumosu

Posted on

Clean Architecture in C#

As I continue my journey into backend development, one question keeps bugging me: Why can’t we just write all the logic inside the controller? It feels so simple. grabbing the data, applying some rules, and updating the database, all in one place. Oh, and I did, but I learnt it wasn't efficient for big companies, and you can't break into big companies with code like this, but I never really understood it or why

But here’s the reality: while that might be fine for a small side project, in large-scale applications (like financial systems) it quickly turns into chaos. That’s why big companies insist on using layered architecture. Splitting responsibilities into Controllers, Business Logic (Services), and Repositories.

For the longest time, I didn’t fully grasp why this separation mattered. I researched, experimented, and honestly struggled until it finally clicked. Once I understood, it completely changed how I think about structuring my software. And because I know many others are walking the same path, I want to share this knowledge in layman’s terms so it finally makes sense.

Let’s dive in with simple analogies, clear diagrams, and practical code to make it stick.


Think of Your App Like a Company

Layer Who It Is What It Does
Controller Front Desk Clerk Takes in requests and passes them along
Service Bank Manager Applies rules, makes decisions, enforces policy
Repository Records Clerk Fetches and updates records in the system
ILogger Security Camera Keeps a history of what happened (audits/logs)
Serilog/NLog Storage Room Decides where those logs are stored

The Flow (Diagram)


[ User Request ]

      ↓

[ Controller ] → Receives request and passes it along to the Services

      ↓

[ Service ] → Applies business rules and logic

      ↓

[ Repository ] → Talks to the database (fetching and updating details only)

Enter fullscreen mode Exit fullscreen mode

Controller: The Front Desk

The controller is the entry point. It doesn’t make decisions or talk to the database; it just receives requests and forwards them.


[ApiController]

[Route("api/transfer")]

public class TransferController : ControllerBase

{

   private readonly ITransferService _service;



   public TransferController(ITransferService service)

   {

       _service = service;

   }



   [HttpPost]

   public IActionResult TransferMoney([FromBody] TransferRequest request)

   {

       _service.Transfer(request.FromAccount, request.ToAccount, request.Amount);

       return Ok("Transfer completed");

   }

}

Enter fullscreen mode Exit fullscreen mode

Service: The Bank Manager

The service (business logic layer) applies rules. For example, “Does the sender have enough money to transfer?”


public class TransferService : ITransferService

{

   private readonly IAccountRepository _repository;

   private readonly ILogger<TransferService> _logger;



   public TransferService(IAccountRepository repository, ILogger<TransferService> logger)

   {

       _repository = repository;

       _logger = logger;

   }



   public void Transfer(string fromAcc, string toAcc, decimal amount)

   {

       var from = _repository.GetAccount(fromAcc);

       var to = _repository.GetAccount(toAcc);



       if (from.Balance < amount)

       {

           _logger.LogWarning("Insufficient funds for account {Account}", fromAcc);

           throw new Exception("Insufficient funds");

       }



       from.Balance -= amount;

       to.Balance += amount;



       _repository.UpdateAccount(from);

       _repository.UpdateAccount(to);



       _logger.LogInformation("Transferred {Amount} from {From} to {To}", amount, fromAcc, toAcc);

   }

}

Enter fullscreen mode Exit fullscreen mode

Repository: The Records Clerk

The repository is where data access happens. No rules, no decisions, just reading and writing data.


public class AccountRepository : IAccountRepository

{

   private readonly BankingDbContext _context;

   private readonly ILogger<AccountRepository> _logger;



   public AccountRepository(BankingDbContext context, ILogger<AccountRepository> logger)

   {

       _context = context;

       _logger = logger;

   }



   public Account GetAccount(string accountId)

   {

       _logger.LogInformation("Fetching account {AccountId}", accountId);

       return _context.Accounts.FirstOrDefault(a => a.Id == accountId);

   }



   public void UpdateAccount(Account account)

   {

       _context.Accounts.Update(account);

       _context.SaveChanges();

       _logger.LogInformation("Updated account {AccountId}", account.Id);

   }

}

Enter fullscreen mode Exit fullscreen mode

Logging: The Security Camera

In finance, logging is crucial. Every transaction, success, or error must be tracked. That’s where ILogger comes in.


_logger.LogInformation("Transaction {TransactionId} completed at {Time}", transaction.Id, DateTime.UtcNow);

_logger.LogError("Failed to process transaction {TransactionId}", transaction.Id);

Enter fullscreen mode Exit fullscreen mode

But ILogger is just an interface. You still need a logging provider like Serilog or NLog to decide where to store those logs (file, cloud, DB, etc.).


// Program.cs setup example with Serilog

Log.Logger = new LoggerConfiguration()

   .WriteTo.File("logs/transactions.txt")

   .CreateLogger();



builder.Host.UseSerilog();

Enter fullscreen mode Exit fullscreen mode

Summary: Who Does What

Layer Responsibility Analogy
Controller Accepts requests and returns responses Front Desk Clerk
Service Applies business rules Bank Manager
Repository Handles data access Records Clerk
ILogger Records actions, errors, and audits Security Camera
Serilog/NLog Stores logs (file, DB, cloud, etc.) Storage Room

Big Companies vs Small Projects

  • Small projects: You might mix everything in the controller. It works for a while.

  • Big companies: This structure is mandatory. It makes apps:

    • Easier to test
    • Easier to scale
    • Easier to maintain
    • Safer (especially for money-related systems)

View it this way:

  • If it’s about handling API requests → Controller
  • If it’s about rules, logic, or decisions → Service
  • If it’s about fetching or saving data → Repository
  • If it’s about tracking activity → ILogger (with Serilog/NLog to store it)

Final Thoughts

If you’re hacking together a side project, you can cut corners. But if you’re aiming to work in big companies or build software that can grow and last, this layered architecture is the foundation you must follow.

Always Think About: Front desk clerk, bank manager, records clerk, and security camera, each with their own role. This helps you remember and know under which classification/section a code is supposed to be. Keeping them separate makes your system stay clean, scalable, and professional.

Did this post help simplify things for you?

If yes, drop a ❤️ or 🦄 reaction and follow me here on dev.to. I share more practical, plain-English breakdowns like this.

You can also connect with me on social media. I’d love to learn, share, and grow together with you!

LinkedIn: LinkedIn
Twitter: Twitter
Instagram: Instagram
Graphics Credit: Ramesh Fadatare

Top comments (3)

Collapse
 
iamcymentho profile image
Odumosu Matthew

Solid breakdown! As a senior .NET engineer, I’ve seen firsthand how ignoring these boundaries in early stages always leads to painful rewrites down the line. Your “front desk clerk / bank manager / records clerk” analogy is spot-on. It's a great mental model for anyone still wrestling with why layering matters.

One small addition I’d make from experience: well-structured architecture doesn’t just help big teams, it helps growing teams. What starts as a solo project can quickly turn into a 10-dev team, and having controllers jammed with logic becomes a nightmare. Clean architecture buys you flexibility, testability, and sanity.

Really appreciate how you broke it down in plain language, more of this kind of content is what the community needs. 👏

Collapse
 
omotola_odumosu_ profile image
Omotola Odumosu • Edited

Thanks so much for the kind words, @iamcymentho 🙌. I completely agree with you. What feels like a small side project today can easily grow into a significant product tomorrow. That’s why it pays to follow best practices from the start, no matter the project size.

It’s these habits that turn us into better developers. And years down the line, when maintainability and scalability become critical, both the brand and the developers inheriting the codebase will thank us for it.

Collapse
 
prime_1 profile image
Roshan Sharma

Great post! 👍
I really like how you break down the layers (Controller / Service / Repository) with analogies that make sense. Helps seeing why big companies push for that separation, cleaner code, easier to test & maintain.