{"id":79903,"date":"2026-06-19T11:40:18","date_gmt":"2026-06-19T06:10:18","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=79903"},"modified":"2026-06-19T12:05:27","modified_gmt":"2026-06-19T06:35:27","slug":"learn-onion-architecture-as-a-complete-beginner-and-implement-as-an-architect","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/learn-onion-architecture-as-a-complete-beginner-and-implement-as-an-architect\/","title":{"rendered":"Learn Onion Architecture as a Complete Beginner and Implement as an Architect"},"content":{"rendered":"<h2>The Problem Story<\/h2>\n<p>Picture this. It is a Tuesday afternoon. Your product manager says: <em>&#8220;We just need to add an email field. Should be quick, right?&#8221;<\/em><\/p>\n<p>You open the codebase. The <code>User<\/code> entity imports <code>SqlConnection<\/code>. Business logic calls the database in the same method. No interfaces. No tests. Everything wired to everything else.<\/p>\n<p>You add the field. The ORM breaks. You fix the ORM, and three unrelated features stop working. Four hours for a twenty-minute task.<\/p>\n<blockquote><p><strong>&#8220;What if your business logic never had to care what database you used, what UI framework you picked, or what cloud you deployed to?&#8221;<\/strong><\/p><\/blockquote>\n<p>That is Onion Architecture \u2014 your business rules at the centre, infrastructure as swappable details at the edge.<\/p>\n<h2>Why Traditional Layered Architecture Fails<\/h2>\n<p>Most developers start with the classic three-tier model: <strong>UI \u2192 Business Logic \u2192 Data Layer<\/strong>. On small projects, it works. On large ones, it becomes a trap in four specific ways:<\/p>\n<ul>\n<li><strong>Tight Coupling<\/strong> \u2014 Business logic directly instantiates database classes. Swap the ORM, and every service class needs surgery.<\/li>\n<li><strong>Difficult Testing<\/strong> \u2014 To test one business rule, you need a real database running. Not because the rule touches it, but because the class containing it does.<\/li>\n<li><strong>Slow Feature Changes<\/strong> \u2014 Adding a feature in the business layer requires understanding the data layer. Teams constantly step on each other.<\/li>\n<li><strong>High Maintenance Cost<\/strong> \u2014 After 18 months, no one wants to touch the code. This is the layered architecture death spiral.<\/li>\n<\/ul>\n<pre>\/\/ The classic anti-pattern: business logic importing infrastructure\r\npublic class OrderService\r\n{\r\n    public void PlaceOrder(Order order)\r\n    {\r\n        using var connection = new SqlConnection(\"Server=...\");\r\n        \/\/ business rules mixed with database calls\r\n    }\r\n}<\/pre>\n<p><!-- INSERT IMAGE: Fig 1 \u2014 Traditional 3-Layer Architecture --><\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\" wp-image-79897\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-1024x356.jpg\" alt=\"Fig 1\" width=\"691\" height=\"240\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-1024x356.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-300x104.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-768x267.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-1536x534.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-2048x712.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-1_HighRes-624x217.jpg 624w\" sizes=\"(max-width: 691px) 100vw, 691px\" \/><\/p>\n<h2><\/h2>\n<h2>The Core Insight: Inward Dependency Rule<\/h2>\n<p>Jeffrey Palermo introduced Onion Architecture in 2008. The single governing rule: <strong>dependencies always point inward.<\/strong><\/p>\n<p>Inner layers know nothing about outer layers. Outer layers adapt to inner layers \u2014 never the reverse.<\/p>\n<p>Think of a company. The CEO (Domain) sets the rules. Management (Application) executes them. Operations (Infrastructure) handles the tools. Front-of-house (Presentation) faces the world. When a new CRM is adopted, the CEO&#8217;s strategy does not change.<\/p>\n<div id=\"attachment_79898\" style=\"width: 704px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-79898\" decoding=\"async\" loading=\"lazy\" class=\" wp-image-79898\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-1024x178.jpg\" alt=\"Fig 2\" width=\"694\" height=\"121\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-1024x178.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-300x52.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-768x134.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-1536x267.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-2048x356.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-2_HighRes-624x109.jpg 624w\" sizes=\"(max-width: 694px) 100vw, 694px\" \/><p id=\"caption-attachment-79898\" class=\"wp-caption-text\">Fig 2<\/p><\/div>\n<p><!-- INSERT IMAGE: Fig 2 \u2014 Dependency Direction --><\/p>\n<p>This works via the <strong>Dependency Inversion Principle<\/strong>: the Application layer defines an interface (<code>IOrderRepository<\/code>), and Infrastructure implements it. The interface lives in the inner layer. The implementation lives in the outer layer.<\/p>\n<p>&nbsp;<\/p>\n<h2>The Four Layers Explained for Everyone<\/h2>\n<p>Using a restaurant analogy: Domain = recipe book, Application = kitchen manager, Infrastructure = ingredients supplier, Presentation = waiter.<\/p>\n<h3>Domain Layer (Innermost)<\/h3>\n<p>The heart of your application: entities, value objects, domain events, pure business rules. No NuGet packages. No framework references.<\/p>\n<ul>\n<li><strong>Lives here:<\/strong> Entities, Value Objects, Domain Events, Business Rules<\/li>\n<li><strong>Never here:<\/strong> Entity Framework, HttpClient, SqlConnection<\/li>\n<\/ul>\n<h3>Application Layer<\/h3>\n<p>The use-case layer. Orchestrates the domain. Contains use cases, service interfaces, and DTOs.<\/p>\n<ul>\n<li><strong>Lives here:<\/strong> Use Cases, IRepository interfaces, DTOs, Application Services<\/li>\n<li><strong>Never here:<\/strong> EF DbContext, HTTP Controllers<\/li>\n<\/ul>\n<h3>Infrastructure Layer<\/h3>\n<p>The plumbing. Implements the interfaces defined in Application. Databases, email, file system, APIs. Completely swappable.<\/p>\n<ul>\n<li><strong>Lives here:<\/strong> SqlOrderRepository, AppDbContext, SmtpEmailService, S3FileStorage<\/li>\n<li><strong>Never here:<\/strong> Business rules<\/li>\n<\/ul>\n<h3>Presentation Layer (Outermost)<\/h3>\n<p>The entry point. Controllers, Minimal API, gRPC, Worker Services. Takes the order, passes it in, returns results. Never contains business logic.<\/p>\n<h2>The 7 Key Principles Beyond the Definition<\/h2>\n<ol>\n<li><strong>Domain-Centric Design<\/strong> \u2014 Business rules first. Database shaped to match the domain, not vice versa.<\/li>\n<li><strong>Dependency Inward Rule<\/strong> \u2014 A <code>using<\/code> statement in Domain pointing to Infrastructure is a violation.<\/li>\n<li><strong>Separation of Concerns<\/strong> \u2014 Each layer, one job. Application orchestrates; it does not calculate.<\/li>\n<li><strong>Infrastructure as External Detail<\/strong> \u2014 Application defines <code>IOrderRepository<\/code>; Infrastructure delivers <code>SqlOrderRepository<\/code>.<\/li>\n<li><strong>Testability<\/strong> \u2014 Use cases testable with nothing but mocks. No database, no HTTP, no test containers.<\/li>\n<li><strong>Maintainability<\/strong> \u2014 Swapping ORM from EF Core to Dapper: one-project change. Application and Domain untouched.<\/li>\n<li><strong>Independent Domain Layer<\/strong> \u2014 Open your domain entity. Any <code>[Key]<\/code> or EF attribute? Remove them.<\/li>\n<\/ol>\n<h3>Self-Assessment Checklist<\/h3>\n<ul>\n<li>Domain entities have zero infrastructure using statements<\/li>\n<li>All interfaces live in the Application layer<\/li>\n<li>Infrastructure references Application \u2014 not the reverse<\/li>\n<li>Domain has no third-party NuGet dependencies<\/li>\n<li>Business rules testable without a database<\/li>\n<li>Swapping the ORM only affects Infrastructure<\/li>\n<li>Presentation calls Application services, never Infrastructure directly<\/li>\n<\/ul>\n<h2>Visualising Onion Architecture<\/h2>\n<p>Fig 3: The Onion rings annotated with real class examples \u2014 Domain at centre, through Application, Infrastructure, to Presentation.<\/p>\n<p><!-- INSERT IMAGE: Fig 3 \u2014 Concentric Rings --><\/p>\n<div id=\"attachment_79899\" style=\"width: 635px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-79899\" decoding=\"async\" loading=\"lazy\" class=\"size-large wp-image-79899\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-1024x639.jpg\" alt=\"Fig 3\" width=\"625\" height=\"390\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-1024x639.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-300x187.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-768x479.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-1536x958.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-2048x1278.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-3_HighRes-624x389.jpg 624w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-79899\" class=\"wp-caption-text\">Fig 3<\/p><\/div>\n<p><!-- INSERT IMAGE: Fig 4 \u2014 Request Flow --><\/p>\n<p><em>Fig 4: A single HTTP POST flowing through all four layers: Controller \u2192 UseCase \u2192 Domain \u2192 Repository \u2192 SQL Server \u2192 201 response.<\/em><\/p>\n<p><!-- INSERT IMAGE: Fig 5 \u2014 .NET Solution Structure --><\/p>\n<div id=\"attachment_79896\" style=\"width: 635px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-79896\" decoding=\"async\" loading=\"lazy\" class=\"size-large wp-image-79896\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-1024x223.jpg\" alt=\"Fig 4\" width=\"625\" height=\"136\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-1024x223.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-300x65.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-768x167.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-1536x334.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-2048x445.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig_4_HighRes-624x136.jpg 624w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-79896\" class=\"wp-caption-text\">Fig 4<\/p><\/div>\n<p><em>Fig 5: Recommended .NET solution structure \u2014 each layer as a separate project for enforced dependency rules.<\/em><\/p>\n<div id=\"attachment_79900\" style=\"width: 687px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-79900\" decoding=\"async\" loading=\"lazy\" class=\" wp-image-79900\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-1024x452.jpg\" alt=\"Fig 5\" width=\"677\" height=\"299\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-1024x452.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-300x132.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-768x339.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-1536x678.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-2048x904.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-5_HighRes-624x275.jpg 624w\" sizes=\"(max-width: 677px) 100vw, 677px\" \/><p id=\"caption-attachment-79900\" class=\"wp-caption-text\">Fig 5<\/p><\/div>\n<h2>Implementing It: From Concept to Code<\/h2>\n<h3>Step 1 \u2014 Domain Entity<\/h3>\n<pre>public class Order\r\n{\r\n    public Guid Id { get; private set; }\r\n    public decimal TotalAmount { get; private set; }\r\n\r\n    private Order() { }\r\n\r\n    public static Order Create(Guid customerId, decimal amount)\r\n    {\r\n        if (amount &lt;= 0) throw new DomainException(\"Amount must be positive.\");\r\n        return new Order { Id = Guid.NewGuid(), TotalAmount = amount };\r\n    }\r\n}<\/pre>\n<h3>Step 2 \u2014 Use Case in Application Layer<\/h3>\n<pre>public class PlaceOrderUseCase\r\n{\r\n    private readonly IOrderRepository _orders;\r\n    private readonly IEmailService _email;\r\n   public async Task&lt;Guid&gt; ExecuteAsync(CreateOrderDto dto)\r\n    {\r\n        var order = Order.Create(dto.CustomerId, dto.Amount);\r\n        await _orders.AddAsync(order);\r\n        await _orders.SaveChangesAsync();\r\n        await _email.SendOrderConfirmationAsync(dto.CustomerEmail, order.Id);\r\n        return order.Id;\r\n    }\r\n}<\/pre>\n<h3>Step 3 \u2014 Controller in Presentation<\/h3>\n<pre>[HttpPost]\r\npublic async Task&lt;IActionResult&gt; Create(CreateOrderDto dto)\r\n{\r\n    var orderId = await _placeOrder.ExecuteAsync(dto);\r\n    return CreatedAtAction(nameof(Create), new { id = orderId }, orderId);\r\n}<\/pre>\n<h3>Step 4 \u2014 DI Wiring (Program.cs)<\/h3>\n<pre>builder.Services.AddScoped&lt;IOrderRepository, SqlOrderRepository&gt;();\r\nbuilder.Services.AddScoped&lt;IEmailService, SmtpEmailService&gt;();\r\nbuilder.Services.AddScoped&lt;PlaceOrderUseCase&gt;();<\/pre>\n<h2>Real-World Use Cases<\/h2>\n<blockquote><p><strong>&#8220;Your business logic should outlive your framework. Onion Architecture is the insurance policy.&#8221;<\/strong><\/p><\/blockquote>\n<h3>E-Commerce<\/h3>\n<p>Discount rules, inventory, order lifecycle independent of storefront or database. Migrate from SQL Server to Cosmos DB \u2014 Domain and Application untouched.<\/p>\n<h3>Banking \/ FinTech<\/h3>\n<p>Compliance rules testable, auditable, independent of vendor SDKs. Business rules live once in the domain \u2014 regardless of web, mobile, API, or batch delivery.<\/p>\n<h3>Multi-Channel Delivery<\/h3>\n<p><!-- INSERT IMAGE: Fig 6 \u2014 Multi-Channel Delivery --><\/p>\n<p>Same Domain and Application layers powering Web API, background worker, CLI tool, and gRPC service. Only Presentation changes per channel.<\/p>\n<div id=\"attachment_79901\" style=\"width: 635px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-79901\" decoding=\"async\" loading=\"lazy\" class=\"size-large wp-image-79901\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-1024x350.jpg\" alt=\"Fig 6\" width=\"625\" height=\"214\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-1024x350.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-300x103.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-768x263.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-1536x525.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-2048x700.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-6_HighRes-624x213.jpg 624w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-79901\" class=\"wp-caption-text\">Fig 6<\/p><\/div>\n<h3>Legacy Migration<\/h3>\n<p>Use the strangler fig pattern \u2014 wrap legacy code with clean interfaces, replace internals gradually. No big-bang rewrite required.<\/p>\n<h2>Onion vs Clean Architecture: The Honest Comparison<\/h2>\n<p><strong>Clean Architecture<\/strong> (Uncle Bob, 2012): conceptual principles \u2014 Entities, Use Cases, Interface Adapters, Frameworks.<\/p>\n<p><strong>Onion Architecture<\/strong> (Palermo, 2008): structural implementation \u2014 Domain, Application, Infrastructure, Presentation with inward dependency rule.<\/p>\n<table>\n<tbody>\n<tr>\n<th>Dimension<\/th>\n<th>Onion Architecture<\/th>\n<th>Clean Architecture<\/th>\n<\/tr>\n<tr>\n<td>Nature<\/td>\n<td>Structural<\/td>\n<td>Conceptual<\/td>\n<\/tr>\n<tr>\n<td>Focus<\/td>\n<td>Domain-centric<\/td>\n<td>Use-case-driven<\/td>\n<\/tr>\n<tr>\n<td>Dependency Rule<\/td>\n<td>Inward<\/td>\n<td>Inward (same)<\/td>\n<\/tr>\n<tr>\n<td>Flexibility<\/td>\n<td>Flexible naming<\/td>\n<td>Prescriptive naming<\/td>\n<\/tr>\n<tr>\n<td>Coexist?<\/td>\n<td colspan=\"2\">Yes \u2014 many teams blend both successfully<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Myth-buster:<\/strong> They are not the same thing. Onion is structural; Clean is philosophical. A well-implemented Onion Architecture satisfies most Clean Architecture principles automatically.<\/p>\n<h2>Things I Wish I Knew Before Implementing Onion Architecture<\/h2>\n<h3>Anti-Pattern 1 \u2014 Infrastructure Leaking into Domain<\/h3>\n<p><code>[Table]<\/code>, <code>[Column]<\/code>, or EF navigation properties on domain entities. Technically compiles. Architecturally fatal.<\/p>\n<pre>\/\/ WRONG\r\n[Table(\"orders\")] public class Order { [Key] public Guid Id { get; set; } }\r\n\r\n\/\/ RIGHT \u2014 EF mapping in Infrastructure, domain stays clean\r\npublic class Order { public Guid Id { get; private set; } }<\/pre>\n<h3>Anti-Pattern 2 \u2014 Anemic Domain Model<\/h3>\n<p>Entities with only getters and setters. Business rules pushed into services. Domain is hollow. Onion without a rich domain is just folder organisation.<\/p>\n<h3>Anti-Pattern 3 \u2014 Service Layer Overload<\/h3>\n<p>Use cases should be small: <code>PlaceOrderUseCase<\/code>, <code>CancelOrderUseCase<\/code>. An 800-line <code>OrderService<\/code> has too many jobs.<\/p>\n<h3>Anti-Pattern 4 \u2014 Interface for Everything<\/h3>\n<p>Interfaces earn their place when they enable mocking or multiple implementations. If a class will only ever have one implementation, the interface is noise.<\/p>\n<h3>Anti-Pattern 5 \u2014 Onion in Name Only<\/h3>\n<p>Check your <code>.csproj<\/code> files: Domain \u2192 nothing, Application \u2192 Domain only, Infrastructure \u2192 Application, API \u2192 Infrastructure + Application. Any other reference is a violation.<\/p>\n<h2>Testing Strategy in Onion Architecture<\/h2>\n<div id=\"attachment_79902\" style=\"width: 635px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-79902\" decoding=\"async\" loading=\"lazy\" class=\"size-large wp-image-79902\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-1024x479.jpg\" alt=\"Fig 7\" width=\"625\" height=\"292\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-1024x479.jpg 1024w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-300x140.jpg 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-768x359.jpg 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-1536x718.jpg 1536w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-2048x957.jpg 2048w, \/blog\/wp-ttn-blog\/uploads\/2026\/05\/Fig-7_HighRes-624x292.jpg 624w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><p id=\"caption-attachment-79902\" class=\"wp-caption-text\">Fig 7<\/p><\/div>\n<p><!-- INSERT IMAGE: Fig 7 \u2014 Testing Pyramid --><\/p>\n<ul>\n<li><strong>Domain layer:<\/strong> Pure unit tests \u2014 no mocks needed, business rules in complete isolation<\/li>\n<li><strong>Application layer:<\/strong> Unit tests with mocked repositories \u2014 no database, no HTTP<\/li>\n<li><strong>Infrastructure layer:<\/strong> Integration tests with real DB (test containers)<\/li>\n<li><strong>Presentation layer:<\/strong> End-to-end \/ contract tests<\/li>\n<\/ul>\n<pre>\/\/ No SQL Server started. Pure business logic test.\r\n[Fact]\r\npublic async Task Execute_NegativeAmount_ThrowsDomainException()\r\n{\r\n    var useCase = new PlaceOrderUseCase(_orderRepo.Object, _email.Object);\r\n    var dto = new CreateOrderDto { Amount = -50m };\r\n    await Assert.ThrowsAsync&lt;DomainException&gt;(() =&gt; useCase.ExecuteAsync(dto));\r\n}<\/pre>\n<h3>Architecture Tests<\/h3>\n<pre>[Fact]\r\npublic void Domain_Should_Not_Reference_Infrastructure()\r\n{\r\n    var result = Types.InAssembly(typeof(Order).Assembly)\r\n        .ShouldNot().HaveDependencyOn(\"YourApp.Infrastructure\").GetResult();\r\n    result.IsSuccessful.Should().BeTrue();\r\n}<\/pre>\n<h2>The Architect&#8217;s Mental Model<\/h2>\n<p>We traced the full journey: tight coupling pain \u2192 inward dependency insight \u2192 four layers \u2192 seven principles \u2192 implementation \u2192 use cases \u2192 comparison \u2192 anti-patterns \u2192 testing.<\/p>\n<blockquote><p><strong>&#8220;Your domain should be able to run its tests with zero infrastructure. If it cannot, your architecture has a leak.&#8221;<\/strong><\/p><\/blockquote>\n<h3>Further Reading<\/h3>\n<ul>\n<li>Jeffrey Palermo&#8217;s original 2008 blog post on Onion Architecture<\/li>\n<li><em>Clean Architecture<\/em> \u2014 Robert C. Martin (2017)<\/li>\n<li><em>Domain-Driven Design<\/em> \u2014 Eric Evans<\/li>\n<li><em>Patterns of Enterprise Application Architecture<\/em> \u2014 Martin Fowler<\/li>\n<\/ul>\n<hr \/>\n<p><strong>Which layer do you think most teams get wrong first, and why?<\/strong> Share your experience in the comments.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Problem Story Picture this. It is a Tuesday afternoon. Your product manager says: &#8220;We just need to add an email field. Should be quick, right?&#8221; You open the codebase. The User entity imports SqlConnection. Business logic calls the database in the same method. No interfaces. No tests. Everything wired to everything else. You add [&hellip;]<\/p>\n","protected":false},"author":2282,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":10},"categories":[5867],"tags":[2930,6490,6734,8604,7248,8603],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/79903"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2282"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=79903"}],"version-history":[{"count":6,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/79903\/revisions"}],"predecessor-version":[{"id":80163,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/79903\/revisions\/80163"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=79903"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=79903"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=79903"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}