<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by iamprovidence on Medium]]></title>
        <description><![CDATA[Stories by iamprovidence on Medium]]></description>
        <link>https://medium.com/@iamprovidence?source=rss-2e55517ec6ff------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*5_4tdBAsyyi0qA4XgMD1xA.png</url>
            <title>Stories by iamprovidence on Medium</title>
            <link>https://medium.com/@iamprovidence?source=rss-2e55517ec6ff------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 05:53:00 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@iamprovidence/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Architecture Scales]]></title>
            <link>https://medium.com/@iamprovidence/architecture-scales-4ddaf42d1a18?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/4ddaf42d1a18</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-architecture]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Tue, 07 Jul 2026 10:01:02 GMT</pubDate>
            <atom:updated>2026-07-07T10:01:02.932Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P9BDGQIWjjYwiHB_P2xBeQ.png" /></figure><p>There has been a lot of confusion over the years around the terms <em>design</em> and <em>architecture</em>. What is software design? What is software architecture? Where’s the line between the two — and does it even exist? Some developers say there is; others see no difference at all. Ultimately, it’s up to you how you interpret it. My goal here is simply to clarify the concepts for you😅.</p><p>Recognizing the different scales of architecture can help you make smarter decisions. That’s why it’s important to understand the distinctions between:</p><ul><li>design principles</li><li>design patterns</li><li>software design</li><li>software architecture</li><li>architecture patterns</li><li>architectural styles</li><li>software architecture</li></ul><p>Each has its place — once you see how they relate, everything becomes much clearer 😉.</p><h4>🎯 <strong>Design Principles</strong></h4><p>This is a bit out of scope of this article, but let&#39;s quickly mention design principles.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3JStwaY7G772tvmLi3Vevg.png" /></figure><p>They help you make smarter decisions long before code becomes a tangled mess.</p><blockquote><strong><em>Design principles</em></strong><em> provide guidelines on how to write clean, maintainable, and scalable </em>code</blockquote><p>Examples:</p><ul><li><a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#62cb">Divide and Conquer</a> — split the problem into smaller parts, solve each part, and combine the results</li><li><a href="https://medium.com/@iamprovidence/the-main-secret-of-software-architecture-6d153b85257c">Separation of Concerns</a> — divide a program into distinct sections, each addressing a separate concern</li><li><a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#2d8c">SOLID </a>— five principles that promote maintainable object-oriented design</li><li><a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#afe0">KISS (Keep It Simple, Stupid<em>)</em></a> — avoid unnecessary complexity</li><li><a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#e3a3">DRY (Don’t Repeat Yourself)</a>— reduce code duplication</li><li><a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#78db">YAGNI (You Aren’t Gonna Need It)</a>— don’t add features until they’re actually needed</li></ul><h4>🧱 <strong>Design Patterns</strong></h4><p>Every program has to start somewhere. Usually from spaghetti code😅. And don&#39;t lie to me. Everybody was there at some point.</p><p>Some parts of the code become unsupportable, so you have to structure those in a better way.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*kFs0mvDmzvUWG-GgdGtKpw.gif" /></figure><p>This is the point where developers start a journey to architecture and learn about design patterns.</p><blockquote><strong><em>Design patterns</em></strong><em> are well-known, proven solutions to common problems encountered in object-oriented design. They operate </em>at the class or object level</blockquote><p>Examples:</p><ul><li>singleton — ensures only one instance of a class exists</li><li>observer — notifies multiple objects about state changes in another object</li><li>decorator — adds new behavior to objects dynamically</li><li><a href="https://medium.com/@iamprovidence/strategy-vs-state-pattern-6168cc102c91">state </a>— allows an object to alter its behavior when its internal state changes</li><li>proxy — controls access to another object, adding a layer like lazy loading, access control, <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84">cache</a>, or <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logging</a></li><li>adapter —converts one interface to another expected by the client</li><li>facade — provides a simplified interface to a complex subsystem</li></ul><h4>🧩 <strong>Software Design</strong></h4><p>Once you’ve established communication between certain classes, you may begin extracting code into a separate project.</p><p>Often, the internal structure of these modules doesn’t follow a specific architecture — the main goal is simply to maintain <a href="https://medium.com/@iamprovidence/77d06d44af0d">low coupling and high cohesion</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/1*ZqvCqcasyt7NCekWPg-Dzg.gif" /></figure><p>However, that’s not always sufficient. Sometimes, the entire module must be deliberately designed in a particular way.</p><blockquote><strong><em>Software design (code architecture)</em></strong><em> refers to low-level structure of </em>a single module</blockquote><p>Examples:</p><ul><li>organizing a <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43"><strong><em>command-handler pattern</em></strong></a> inside the Application module to separate concerns</li><li>using a <a href="https://medium.com/@iamprovidence/understanding-object-oriented-design-rich-vs-anemic-models-in-c-5d8e5ea1bf39"><strong>rich domain model</strong></a><strong> </strong>to enforce consistency and encapsulate business logic within the <a href="https://medium.com/@iamprovidence/55f35ab38f45">Domain</a> project</li><li>applying <a href="https://medium.com/@iamprovidence/d96bdd6a526f"><strong>screaming architecture</strong></a> to make the component’s intent and purpose clearly reflected in its structure and naming</li><li>designing a <a href="https://medium.com/@iamprovidence/strategy-vs-state-pattern-6168cc102c91"><strong>state machine</strong></a> to manage transitions in a workflow engine</li><li>implementing the <a href="https://medium.com/@iamprovidence/bridge-pattern-without-bullsh-t-38d7d851ed4d"><strong><em>bridge pattern</em></strong></a><em> </em>to enforce a common structure in <a href="https://medium.com/@iamprovidence/reporting-database-pattern-78fc6779f14b">reporting</a> jobs</li></ul><h4>🧠 Architecture Patterns</h4><p>One project is great. But do you know what is even better? Many projects😅.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/1*5m3UHWdI_uRfPO7Pv3xuyA.gif" /></figure><p>Similar to what we had before, we need to organize and structure those modules somehow.</p><blockquote><strong><em>Architecture patterns</em></strong><em> provide guidance on how to organize </em>many projects<em> within a </em>single<em> </em>application</blockquote><p>Examples:</p><ul><li><a href="https://medium.com/@iamprovidence/mvc-vs-mvp-vs-mvvm-with-c-examples-8013745e3c4c">MVC/MVP/MVVM</a> — divides an application into Model, View, and Controller layers to separate business logic, user interface, and control logic</li><li>Monolith<strong> </strong>Architecture — a traditional architectural style where the entire application is built, deployed, and run as a single, unified unit</li><li><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#67ea">N-layered</a> — divides the application into multiple layers with distinct responsibilities</li><li><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#a70e">DDD Layers</a> — focuses on aligning software design with the business domain</li><li><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#8386">Hexagonal Architecture (Ports and Adapters)</a> — isolates business logic from external dependencies</li><li><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#f440">Onion Architecture</a> — enforces dependency flow toward the domain</li><li><a href="https://medium.com/@iamprovidence/e3b7b8f48ce9/">Vertical Slice Architecture</a> — structures the system around features or use cases, keeping all related layers (UI, Domain, Infrastructure) within a single, independent slice</li><li><a href="https://medium.com/@iamprovidence/multitenancy-in-action-f4b110861314">Multitenant Architecture</a> — an architectural approach that enables a single instance of software to serve multiple independent user groups (tenants)</li><li><a href="https://medium.com/@iamprovidence/cqrs-ba4545440a3c">CQRS </a>— separates read and write models for scalability and clarity</li><li><a href="https://medium.com/@iamprovidence/event-sourcing-7dc8bb63db7c">Event Sourcing</a> — stores state as a sequence of events rather than a single snapshot</li></ul><h4>🗺️ Architectural Styles</h4><p>One application is great. But do you know what is even better? 😂 Many applications.</p><p>Systems grow. Now they consist of multiple applications, and they all need to interact with each other.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/1*S3cG60ADdJc4yg73Xw5qbg.gif" /></figure><blockquote><strong><em>Architectural styles</em></strong><em> define the overall structure and interaction of </em>multiple applications</blockquote><p>Examples:</p><ul><li><a href="https://medium.com/@iamprovidence/the-internet-f0db9375ee62#c98e">Client-Server</a> — separates consumers (clients) from providers (servers)</li><li><a href="https://medium.com/@iamprovidence/modular-monolith-6bb4be9232cd">Modular Monolith</a><strong> </strong>— organizes the system into well-defined, independently developed modules within a single deployable unit</li><li><a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">Microservices</a> — compose the system of small, independent services</li><li>REST — defines a resource-oriented interaction style over a uniform interface</li><li><a href="https://medium.com/@iamprovidence/9cc85e8c3795/">Event-Driven Architecture</a> — components interact through events rather than direct calls</li><li><a href="https://medium.com/@iamprovidence/e6e96fab111a/">Serverless</a> — abstracts server management by running functions or services on demand in a cloud environment</li></ul><h4>🏛️ <strong>Software Architecture</strong></h4><p>For the final, we have <em>software architecture</em>. It dictates the shape of the entire system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FTuLajikHSaTUBrhq_-R2w.png" /></figure><p>It is also the most ambiguous term — often used to refer to any of the concepts above, depending on the scale you’re working at.</p><p>After all, architecture can be described in ways that apply across different contexts.</p><blockquote><strong><em>Software architecture</em></strong><em> describe </em>components<em> of a system and the </em>relation<em> between them</em></blockquote><p>What’s fascinating is how very basic design patterns reappear at higher levels of abstraction, seamlessly blending into architectural styles 😅:</p><ul><li>a <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84#d199">distributed cache</a> acts like a <strong>singleton </strong>inside a cluster</li><li>a <strong>facade</strong> pattern morphs into a <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#ac2d">gateway</a></li><li>an <a href="https://medium.com/@iamprovidence/relationships-between-bounded-contexts-in-ddd-ce5cfe3aaa04#46a0">anti-corruption layer</a> is nothing more than the <strong>adapter</strong></li><li>the<strong> mediator</strong> becomes an event bus, while <strong>the observer</strong> is similar to queue (you can also argue that the observer is the webhook approach)</li><li>concepts like <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#ad20">saga</a> represent <a href="https://medium.com/@iamprovidence/strategy-vs-state-pattern-6168cc102c91"><strong>state</strong></a> machines</li></ul><p>I could go on — proxies, decorators, sidecars<strong> </strong>— but you get the idea. The concept remains the same; only the <em>scale </em>changes. That’s why mastering the fundamentals is so essential — it helps you recognize familiar patterns, no matter how big the system gets 🙃.</p><p>This recursive nature of software concepts — repeating at different levels of scale — is what makes architecture both challenging and beautiful 😌.</p><h3><strong>Summary</strong></h3><p>Let me quickly sum it up for you, so can easily compare those terms without scrolling 😁:</p><ul><li><strong>Design Principles</strong> provide guidelines on how to write clean code</li><li><strong>Design Patterns</strong> address common, low-level problems within classes</li><li><strong>Software Design</strong> focuses on the internal structure of one module</li><li><strong>Architecture Patterns</strong> guide the organization of multiple modules in an application</li><li><strong>Architectural Styles</strong> describe the interaction between many applications</li><li><strong>Software Architecture </strong>shapes the whole system layout</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/893/1*I_UuOV47fmqcs43grq7mtA.png" /></figure><p>👏 Give this article a clap if you liked it</p><p>☕️ You can also support me using the link below</p><p>✅ Follow to see more</p><p>🙃 And keep coding</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4ddaf42d1a18" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Serverless Architecture]]></title>
            <link>https://medium.com/@iamprovidence/serverless-architecture-e6e96fab111a?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/e6e96fab111a</guid>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[serverless]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Sat, 27 Jun 2026 10:01:02 GMT</pubDate>
            <atom:updated>2026-06-27T10:01:02.953Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rgB9o4taWVLZMzTozHghCQ.png" /></figure><p>If you’ve been around long enough, you probably started building applications the “classic” way: create a single API, define a few endpoints, and deploy everything onto a server.</p><p>As systems grew, we split that <a href="https://medium.com/@iamprovidence/modular-monolith-6bb4be9232cd">monolith</a> into multiple smaller APIs — <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">microservices</a> — each deployed independently, often across many servers.</p><p>But the idea doesn’t stop there.</p><p>Take it one step further, and you can break an application down into even smaller units — sometimes as small as a single endpoint — and deploy each piece independently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*RrOB_3GbWMaCkVeYVWaD5w.png" /></figure><p>Writing less code sounds nice, but you can easily end up spending more time deploying each piece than actually building the functionality.</p><p>Because of that, many cloud providers (like Azure) simplify <a href="https://medium.com/@iamprovidence/devops-for-devs-4bd70721f6bf">DevOps</a> for us by handling provisioning, <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#1771">scaling</a>, execution, and resource cleanup on our behalf.</p><p>At this point, something interesting happens: you’re no longer thinking about servers at all. Not because they disappeared, but because you no longer have to care about them. This results in a serverless architecture.</p><blockquote><em>Serverless architecture is a cloud computing model where developers build and run applications without managing servers.</em></blockquote><h4>So, How Exactly Does It Work?🤔</h4><p>At the core of serverless is a simple idea: you write small pieces of code (a <strong>function</strong>), and something else decides <strong>when</strong> and <strong>where</strong> to run it.</p><p>The <strong>Function </strong>is a small, stateless unit of work. Each function is focused on doing one thing well, not hosting an entire application.</p><pre>public class MyFunction<br>{<br>    [FunctionName(&quot;MyFunction&quot;)]<br>    public async Task Run(<br>        [HttpTrigger(AuthorizationLevel.Function, &quot;post&quot;)] // 👈 trigger<br>        HttpRequest req) <br>    {<br>         . . .<br>    }<br>}</pre><p>From there, the flow is straightforward:</p><ul><li>An event occurs (<a href="https://medium.com/@iamprovidence/services-communication-http-long-polling-server-sent-events-websocket-graphql-soap-grpc-b2e9b331fa54">HTTP request</a>, queue message, file upload, a scheduled job, or even a database change)</li><li>The cloud platform invokes the corresponding function</li><li>The function runs in an isolated environment</li><li>Resources are allocated automatically for execution</li><li>Once the function completes, everything is cleaned up</li></ul><p>You don’t manage servers, <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#1771">scaling</a> rules, or infrastructure. The platform handles it in real time.</p><h3>Snap Back to Reality</h3><p>I can already see some of you, inspired by “no servers at all” idea, but don’t rush up.</p><p>Functions work great for internal tools, <a href="https://medium.com/@iamprovidence/the-good-the-bad-and-the-ugly-ihostedservice-8be82d063584">background jobs</a>, startups validating ideas, and <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">event-driven systems</a>. They let you move fast, focus on <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">business logic</a>, and avoid heavy DevOps work.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zuZDOwM1UqCGhgubKmGp2g.png" /></figure><p>But they fall short when you need long-running processes, low-latency responses, or complex business logic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ICGm22UQbk2pD_qotNZujg.png" /></figure><p>Serverless is a <em>great building block</em>, but not always a great <em>entire foundation</em>.</p><p>Therefore, it is usually a bad idea to build an entire project on functions. Most real systems are a mix — a traditional monolith API with some serverless parts. Trying to force everything into serverless usually ends in messy code, painful debugging and tracing, hidden costs, and other problems we will discuss below.</p><h4>Long-Running Execution Limits in Serverless</h4><p>One of the most important constraints in serverless architecture is execution time. Even though you would like to move the background work to a function, serverless platforms are built for short-lived, <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">event-driven</a> workloads — not processes that run for minutes or hours. Usually, cloud platforms enforce a maximum execution duration, like up to 5–10 minutes (depending on plan).</p><p>Once this limit is reached, the function is <strong>terminated, </strong>leading the application to an inconsistent state and risking data integrity.</p><p>Because of these limits, long-running workflows must be redesigned:</p><ul><li>break work into small steps</li><li>persist progress externally (database, queue, storage)</li><li>make each step idempotent, and safe to retry</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/466/1*9JfrMXNe8NwdHMua5r89WQ.png" /></figure><p>This isn’t just a technical limitation — it’s a shift in thinking. Serverless doesn’t support long-running execution. It forces you to model long-running <strong>workflows as a series of <em>short</em> tasks</strong>.</p><h4>The Complexity Trap of Serverless</h4><p>Serverless works best when the logic is simple, <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">event-driven</a>, <a href="https://medium.com/@iamprovidence/77d06d44af0d">loosely coupled</a>, and short-lived.</p><p>At first glance, it may seem that serverless makes complex logic easier by breaking it into smaller pieces. That’s only partially true. Serverless doesn’t eliminate complexity — it redistributes it.</p><p>As business logic is split across multiple functions, it often becomes more fragmented and harder to read, reason about, and maintain. On top of that, serverless introduces additional challenges: coordinating between functions, managing state externally, and handling the overhead of distributed workflows.</p><p><strong>— out-of-process by design</strong></p><p>One important shift: <strong>your application is no longer stateful by default</strong>. Functions are ephemeral, so any state or in-memory data must be stored externally.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/949/1*_nJJZuJvIdBQis1V5pFWww.png" /></figure><p>Therefore, you need to rely more on supporting services — databases, authentication providers, storage, and messaging systems — to persist and manage that state.</p><p>Of course, some cloud providers support durable functions, adding orchestration, state persistence, and automatic retries for long-running workflows in a way that’s transparent to the developer, but that does not completely eliminate the need for careful design.</p><p>The in-<a href="https://medium.com/@iamprovidence/what-happens-when-you-double-click-the-exe-file-2f145ce700ad#8152">process</a> mechanisms that worked for a single instance are no longer sufficient. Everything needs to be out-of-<a href="https://medium.com/@iamprovidence/what-happens-when-you-double-click-the-exe-file-2f145ce700ad#8152">process</a> now:</p><ul><li>to <strong>share </strong><a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84"><strong>cache</strong></a> between multiple nodes, it should be <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84#d199">distributed</a></li><li><a href="https://medium.com/@iamprovidence/all-you-need-to-know-about-task-in-c-2dce9e52c0f7"><strong>threads</strong></a><strong> synchronization</strong> can be achieved using the <a href="https://medium.com/@iamprovidence/pessimistic-locking-in-practice-d159e230ebbf#616e">distributed lock</a></li><li><a href="https://medium.com/@iamprovidence/how-to-share-appsettings-json-within-multiple-microservices-c280ae8d587e"><strong>configuration settings</strong></a> need to be consistent across instances</li><li><a href="https://medium.com/@iamprovidence/the-good-the-bad-and-the-ugly-ihostedservice-8be82d063584"><strong>task scheduling</strong></a> must ensure coordination across multiple nodes</li><li>idempotent <strong>request coordination</strong> is a must</li></ul><p>This result is a system with more moving parts — and, inevitably, more potential failure points.</p><p><strong>— communication between components</strong></p><p>In a monolith handling long-running processes, you can simply call one method after another within the same execution context. In serverless, that model breaks down — each step lives in a separate function, and you must explicitly pass context between them.</p><p>Serverless components don’t communicate directly. Instead, they rely on <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">event-based communication</a>, where context is passed along with events from one function to the next.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/688/1*-hN8brGVQ_8mlBRTlKB9mw.png" /></figure><p>If the context becomes too large to include in event payloads and you want to avoid document events, a common approach is to persist state in a database and use events only to signal that a step has completed and the next one should start.</p><p>While sharing storage is often considered an <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#93bc">anti-pattern in microservices </a>— where each service should own its data — in serverless architectures this is more acceptable. Functions are smaller, more transient units, and using shared storage as a coordination mechanism is often the most practical solution.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/686/1*cLc7u3xVPo_Cxf9E6-yJjg.png" /></figure><p><strong>— transactions</strong></p><p>In a monolith or a traditional service, complex operations often rely on a <strong>single </strong><a href="https://medium.com/@iamprovidence/transactions-in-entity-framework-7775ab91ede1"><strong>transactional</strong></a><strong> boundary</strong>. Multiple steps can be executed atomically — either everything succeeds, or everything is rolled back.</p><p>In serverless, that model no longer applies. There is no built-in way to wrap multiple steps in a single, atomic transaction.</p><p>To maintain consistency across steps, you end up implementing:</p><ul><li>compensating actions — manually undoing previously completed steps</li><li>eventual consistency — accepting that the system may be temporarily inconsistent</li><li>retry logic — handling partial failures and duplicate executions</li></ul><p>What used to be straightforward transactional logic becomes real complexity.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/876/1*ZLSCeUX7S2s_wPpZCbrYew.png" /></figure><p>Overall, if your domain logic is rich and tightly connected, a more traditional service (or even a <a href="https://medium.com/@iamprovidence/modular-monolith-6bb4be9232cd">modular monolith</a>) is often the simpler, more maintainable choice.</p><h4>Cold Start Problem in Lambdas</h4><p>One of the most common downsides of serverless is the <strong>cold start</strong>.</p><p>A cold start happens when your function hasn’t been used for a while, so the platform has to deploy it before running. This does not happen instantly but requires time.</p><p>Although functions are mostly used for asynchronous execution when you don’t expect an immediate response, there are cases when it is not acceptable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*XKdmk8XbNne_BFViVFdDVw.gif" /></figure><p>There are a few practical ways to reduce cold starts:</p><ul><li><strong>keep functions small</strong> — less code, fewer dependencies, and simpler initialization = faster startup</li><li><strong>pay for pre-warmed instances</strong> — most providers let you keep some instances always ready (at an extra cost)</li><li><strong>deploy as a web app</strong> — a bit of a cheat 😅, but running as an app service removes cold starts entirely</li><li><strong>use warm-up strategies</strong> — trigger your function periodically (e.g., every few minutes) to keep it “warm”</li><li><strong>accept it where it doesn’t matter</strong> — <a href="https://medium.com/@iamprovidence/the-good-the-bad-and-the-ugly-ihostedservice-8be82d063584">background jobs</a>, queues, async processing — cold starts are often irrelevant there. <a href="https://medium.com/@iamprovidence/you-should-not-care-about-performance-and-here-is-why-42b327597f6d">Don’t optimize</a> what users never notice</li></ul><p>Cold starts aren’t a dealbreaker — but ignoring them is a mistake. If you’re building anything user-facing, you need to account for them early, not after users start noticing delays.</p><h4><strong>Vendor lock-in</strong></h4><p>The last problem we&#39;re going to discuss is vendor lock-in.</p><p>Applications typically rely on standard protocols like HTTP to stay portable. This makes it possible to swap components, move them to different servers, or even rewrite parts of the system without having to rebuild everything from scratch.</p><p>Serverless is different.</p><p>Once you adopt it, you inevitably start depending on the Cloud platform itself. As you need to adapt to your cloud provider and how it handles:</p><ul><li>function runtime</li><li>bindings (like Azure Functions input/output bindings)</li><li>configuration and deployment style</li><li>built-in integrations (storage, auth, messaging)</li><li>etc</li></ul><p>All of this is tightly coupled. Your function isn’t just “code” anymore — it’s code + platform.</p><p>Vendor lock-in is a problem developers try to deal with. The good news is, it is not a problem😅.</p><p>In practice, you will never change cloud providers. Aiming for a fully independent solution will make you lose most of the benefits and platform features, and you’ll spend a lot of time fighting the platform instead of using it.</p><p>That said, it doesn’t mean you should fall into the trap of putting everything inside a function. You still need to follow clean code principles to avoid ending up with messy, unmanageable code, like below, where you mix concerns:</p><pre>public static class PlaceOrderFunction<br>{<br>    [FunctionName(&quot;PlaceOrder&quot;)]<br>    public static async Task Run(<br>        [HttpTrigger(AuthorizationLevel.Function, &quot;post&quot;)]<br>        HttpRequest req,<br>        <br>        // ⚠️ application logic mixed with infrastructure concerns<br>        [ServiceBus(queueName: &quot;orders-queue&quot;, Connection = &quot;ServiceBusConnection&quot;)]<br>        IAsyncCollector&lt;OrderPlacedEvent&gt; output)<br>    {<br>        // ⚠️ application logic mixed with infrastructure concerns<br>        var userId = new UserId(Guid.Parse(req.HttpContext.User.Value)); // ⚠️<br><br>        var order = new Order(OrderId.New(), userId);<br>        dbContext.Orders.Add(order);<br><br>        await dbContext.SaveChangesAsync();<br>        await output.AddAsync(new OrderPlacedEvent(order.Id, userId)); // ⚠️<br>    }<br>}</pre><p>Keep your business logic clean.</p><p>Isolate platform-dependent parts at the edges of your system, define application-level abstractions, and let the <a href="https://medium.com/@iamprovidence/infrastructure-layer-boundaries-best-practices-and-pitfalls-2210d400db5a">infrastructure layer</a> handle cloud integration.</p><p>Don’t try to eliminate vendor lock-in entirely — manage it instead.</p><pre>// Presentation Layer<br>public class PlaceOrderFunction<br>{<br>    private readonly PlaceOrderUseCase _useCase;<br><br>    public PlaceOrderFunction(PlaceOrderUseCase useCase)<br>    {<br>        _useCase = useCase;<br>    }<br>    <br>    [FunctionName(&quot;PlaceOrder&quot;)]<br>    public async Task Run(<br>        [HttpTrigger(AuthorizationLevel.Function, &quot;post&quot;)] <br>        HttpRequest req<br><br>        // ✅ do not rely on Azure Functions–specific bindings in your core logic<br>        /*IAsyncCollector&lt;OrderPlacedEvent&gt; output*/) <br>    {<br>        await _useCase.ExecuteAsync();<br>    }<br>}<br><br>------------------------------------------------------------------------------------<br><br>// Application Layer<br>public class PlaceOrderUseCase<br>{<br>    private readonly ICurrentUserService _currentUserService;<br>    private readonly IDbContext _dbContext;<br>    private readonly IMessageBus _messageBus;<br><br>    public PlaceOrderUseCase(<br>        ICurrentUserService currentUserService<br>        IDbContext dbContext,<br>        IMessageBus messageBus)<br>    {<br>        _currentUserService = currentUserService;<br>        _dbContext = dbContext;<br>        _messageBus = messageBus;<br>    }<br><br>    public async Task ExecuteAsync()<br>    {<br>        // ✅ application logic uses abstractions<br>        var userId = _currentUserService.GetCurrentUserId(); // ✅<br><br>        var order = new Order(OrderId.New(), userId);<br>        _dbContext.Orders.Add(order);<br><br>        await _dbContext.SaveChangesAsync();<br>        await _messageBus.AddAsync(new OrderPlacedEvent(order.Id, userId)); // ✅<br>    }<br>}</pre><p>From an <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4">architectural</a> perspective, cloud-specific SDKs, configurations, and service integrations belong to the <a href="https://medium.com/@iamprovidence/infrastructure-layer-boundaries-best-practices-and-pitfalls-2210d400db5a">infrastructure layer</a> and should be kept behind clear <a href="https://medium.com/@iamprovidence/b1fafacf7a90/">boundaries</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/825/1*E8OZhepv0l1E2mc1E0l9QQ.png" /></figure><p>Treating the cloud as infrastructure preserves portability, keeps <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">business logic</a> clean, and prevents cloud providers from leaking into the core of your system. That way, even if you ever need to move, you’re not rewriting the whole system — just the parts that actually depend on the cloud.</p><h3>Conclusion</h3><ul><li>Serverless breaks applications into small, independently executed functions managed by the cloud.</li><li>It works best for simple, event-driven, short-lived tasks — not complex or long-running processes.</li><li>It excels at scalability, speed, and operational simplicity, but requires thoughtful design to avoid hidden complexity.</li><li>Serverless does not eliminate the complexity of business logic — it forces you to split it into a smaller series of manageable steps.</li><li>Additionally, you have to deal with orchestration, state management, communication, transactions, and consistency handling.</li><li>Core limitations of serverless are timeouts, cold starts, and vendor lock-in.</li><li>In practice, serverless is a powerful building block, but most real systems work best as a hybrid with traditional architectures.</li><li>Used in the right context, serverless can significantly accelerate development and reduce costs.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rFf0pUnOk2MXNWnue9rQUw.png" /></figure><p>That’s all folks 🐷 I hope you learned something new here 🤓 Give this article a clap 👏 Support me with coffee ☕️ And do not forget to follow to receive more ✅</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e6e96fab111a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Event-Driven Architecture]]></title>
            <link>https://medium.com/@iamprovidence/event-driven-architecture-9cc85e8c3795?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/9cc85e8c3795</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[event-driven-architecture]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Wed, 17 Jun 2026 10:01:03 GMT</pubDate>
            <atom:updated>2026-06-23T07:42:25.760Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NBgJntoCoYOX4NpO4AiEGQ.jpeg" /></figure><p>Picture a common scenario with two <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">microservices</a>: one handling Orders and another managing Users. The Orders microservice needs to verify if a <em>user</em> is eligible to place an order based on their age.</p><p>Since the data is decentralized, the user’s information resides in the Users microservice.</p><p>The most straightforward and obvious solution that comes to mind is simply querying the missing information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9zVLpYrd6mbPjzaZKtszgA.png" /></figure><p>While simple, it adds <a href="https://medium.com/@iamprovidence/you-should-not-care-about-performance-and-here-is-why-42b327597f6d#eb04"><em>latency</em></a><em> </em>because of <a href="https://medium.com/@iamprovidence/the-internet-f0db9375ee62">the network</a> overhead; the communication is synchronous, <em>blocking the client until a response is received</em>; services become dependent on each other’s functionality, resulting in <em>tight coupling</em> (if you move some behavior to another microservice, you need to rewrite all dependent clients), and so on and so forth.</p><p>An alternative approach would be to use events and solve the original problem this way:</p><ul><li>OrdersAPI creates its own projection of [User] table (but only with the columns it needs)</li><li>then OrdersAPI subscribes for changes in UsersAPI</li><li>when the user’s age changes, OrdersAPI updates the corresponding records in its own [User] table</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3JJuv-1HDr73x3QgyXSqCQ.png" /></figure><p>Yes, it means data duplication; however, this time, OrdersAPI is self-sufficient. It has all the data needed to execute an action without making additional requests, without relying on the client microservice to be up and running,<strong> </strong>and without introducing coupling between services.</p><blockquote><em>The approach where the system is designed to communicate by emitting and reacting to events, instead of calling each other directly, is called </em><strong><em>Event-Driven Architecture</em></strong><em>.</em></blockquote><p>Nothing stops you from applying Event-Driven Architecture with CRUD applications, but it is usually a bad fit if strong consistency is a must.</p><p>On the other hand, it works great in distributed systems that require high scalability, loose coupling, and asynchronous workflows.</p><p>Let’s explore the core components that make up an event-driven architecture, examine the key characteristics of each one, and discuss the patterns that can be applied in practice. This will help build a solid understanding of how EDA works and how to design reliable, scalable systems with it 😜.</p><h3>Overview</h3><p>At its core, event-driven architecture models systems around the occurrence of events rather than direct method calls or requests.</p><p>Something happens → An event is published → Listeners react to events.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8MkFxAM9hPvFrYW2NV4w1Q.png" /></figure><p>In this architecture, you can highlight the following components:</p><ul><li><strong>event</strong>— a fact that something happened (UserRegisteredEvent, etc)</li><li><strong>event source (producer/publisher/emitter) </strong>— someone who produces an event</li><li><strong>event listener</strong> <strong>(consumer/subscriber/sink)</strong>— someone who reacts to the event</li><li><strong>event broker</strong> <strong>(message channel)</strong>— a tool through which the source and the listener exchange events</li></ul><p>While it may look simple, let’s take a closer look at each of these concepts, as they are quite different from what we are used to in REST.</p><h3>Events</h3><p>In Event-Driven Architecture (EDA), <strong>events</strong> are facts that describe something that has already happened in the system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1XrhW7ZD4jVlzZG8TpeXPA.png" /></figure><p>Despite the name, an event is just a plain DTO class used as a data container.</p><pre>class UserCreatedEvent : IEvent<br>{<br>    // metadata<br>    public Guid EventId { get; }<br><br>    // payload<br>    public int UserId { get; }<br>    public string UserName { get; }<br>    public string UserSurname { get; }<br>}</pre><p>You can also inherit the marker interface to categorize messages or to apply cross-cutting behavior consistently.</p><h4>Event Immutability</h4><p>One of the most important characteristics is that events are <strong>immutable types</strong>. It derives from their nature, as an event represents something that happened in the past.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*Uu1_gDoiVrNgd8Y3J7KwpQ.png" /></figure><p>But there is also a practical reason for it. It is critical that no one can modify the contents of an event after it has been published; otherwise, it becomes impossible to know what was sent originally.</p><h4>Event Types</h4><p>In Event-Driven Architecture, messages can be classified by different types:</p><ul><li><strong>Message</strong> — a generic term for any piece of information sent between components</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/999/1*S6K8QTbQq4GltIXF28Re-Q.png" /></figure><ul><li><strong>Event</strong> / <strong>Notification</strong>— <em>something happened</em> (UserCreatedEvent)</li><li><strong>Command</strong> — <em>do something</em> (CreateUserCommand)</li><li><strong>Document</strong> — <em>here is the data </em>(UserInfoDocument)</li></ul><p><strong>An event</strong> represents something that happened (UserCreatedEvent). They usually broadcast to multiple consumers. The publisher does not care who consumes it, what actions they take, and does not wait for a response from them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9enb4Y5715Wdv55sohW44A.png" /></figure><p>A<strong> command</strong> instructs the receiver to take a specific action (CreateUserCommand). It is sent to a single receiver — similarly to a REST request, but without exposing the receiver’s API or coupling the sender to its implementation. The publisher may or may not expect a response through the same channel.</p><p>Messages are usually poorly suited for the request-response model, since you need to serialize the message, store it, retrieve and deserialize it back. However, it allows you to shift a heavy task to asynchronous background processing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SoyoliYHAHuBPGXJo_UlRw.png" /></figure><p>A <strong>document message</strong> conveys the entire snapshot of the state<strong>.</strong></p><p>It allows the publisher to ignore how the data is used, what information is needed, or how many consumers will process it. Consumers can process, store, or forward the data without affecting the publisher or needing to coordinate with it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EfUDqeU5-A05z4osPIy0aw.png" /></figure><p>As you might guess, in the Event-Driven Architecture, the most popular and used type is events 😅 . However, commands and document messages also play important roles.</p><blockquote><em>⚠️ By the way ⚠️</em></blockquote><blockquote><em>Some projects may adopt different naming conventions or even assign their own terms to existing concepts — for example, some teams call an </em><em>Event a </em><em>Message. Therefore it is important to make sure, you understand their language, to avoid confusion and keep communication clear.</em></blockquote><h4>Event Semantic</h4><p>Events can be categorized by their usage:</p><ul><li>domain events — are used to communicate changes and trigger reactions <strong>inside </strong>the application within the current bounded context</li><li>integration events — are used to communicate changes and trigger reactions <strong>outside </strong>the application across different bounded contexts</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*825P4qpSIMLPWfT9PLiU8w.png" /></figure><p>Domain events are usually easy to work with since:</p><ul><li>they operate <em>in-process</em> and are primarily used <em>to</em> <em>reduce coupling </em>between components</li><li>they can be <em>published at any moment</em>, as they are part of <em>local transaction</em> boundaries that respect ACID principles</li><li>they typically contain <em>smaller payloads</em>, since event listeners are located within the same service, and their data requirements are clearly defined</li><li>their structure is <em>simpler to change</em>, as versioning is often unnecessary</li><li>they are usually exchanged through an in-memory bus and are <em>not persisted</em>; it is fine to lose them when the application crashes</li></ul><p>Integration events require more careful design because:</p><ul><li>they are <em>out-of-process</em> and are used <em>to notify external systems</em> about the changes in state</li><li>should be <em>published</em> only after the <em>local transaction completes</em>, when the local state has become stable, as they coordinate <em>distributed transactions</em></li><li>they carry more <em>complex payloads</em> with additional attributes, as different listeners may require different information</li><li>their structure is <em>more robust to evolve</em>, as they need to be backward compatible, often requiring versioning</li><li>they must be persisted in a <em>durable</em> out-of-process event bus, as they play a critical role in maintaining data consistency across the entire system</li></ul><p>It is common to model the same business fact as both a domain event (such as UserAgeChanged<strong><em>DomainEvent</em></strong>) and an integration event (for example UserAgeChanged<strong><em>IntegrationEvent</em></strong>), with the integration event being <a href="https://github.com/devmentors/Pacco.Services.Orders/blob/master/src/Pacco.Services.Orders.Infrastructure/Services/EventMapper.cs">mapped</a> from the domain event for external communication.</p><h4>Event Structure</h4><p>An event typically consists of three parts:</p><ul><li><strong>name</strong> — identifies the event and often reflects what happened</li><li><strong>metadata</strong> — contains technical information about the event</li><li><strong>payload</strong> — the actual data describing what happened</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/353/1*2i5il5_WwQK-vVBeAkY0ZA.png" /></figure><p>The event structure should be consistent and applied across all events, allowing consumers to handle them in a predictable way.</p><p><strong>— name</strong></p><p>In Event-Driven Architecture, the <strong>event name</strong> is not just a label — it’s a contract. It tells consumers <em>what exactly happened</em> in the system (UserAgeChanged).</p><p>By convention, event classes are usually named:</p><ul><li>in the <em>past tense</em> (UserCreated, OrderPaid)</li><li>with an <em>Event suffix</em> (UserCreatedEvent)</li></ul><p>This convention is widely used because it makes the intent obvious, but it’s not strictly required.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*sRHt7oUVaFzu5_1nnPEP7A.png" /></figure><p>The event name also influences what must be included in the <em>payload</em>.</p><p>Generic names require richer payloads. An event like UserUpdatedEvent is vague. To make it useful, you typically need to include the previous and new states of an entity, so consumers can understand <em>what actually changed</em>.</p><p>Specific names can minimize payload. An event like UserDeletedEvent may need no payload at all. The name itself already fully describes what happened.</p><p>Additionaly, the name may include <em>metadata</em>, such as a version (UserCreatedEventV2). This can be useful for evolution and backward compatibility, but it should be applied to all events when.</p><p><strong>— metadata</strong></p><p>It is recommended, but not required, for every event to include <strong>metadata</strong> — technical information that makes events reliable, traceable, and safe to process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*L9y2PCboBP1-pNPa0591dw.png" /></figure><p>Medatada typically includes the following properties:</p><ul><li><strong>eventId — </strong>a unique event identifier (usually based on a GUID). Its primary purpose is <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26#a184">audit</a> and <em>idempotency</em>: ensuring that the same event is not processed more than once</li><li><strong>entityId (subjectId)</strong> — the identifier of the entity associated with this event (for example, an order or a user). It can be numeric, but in practice, a <strong><em>string</em></strong> is often chosen as a more generic and implementation-agnostic option</li><li><strong>version</strong> — indicates the <strong><em>schema version</em></strong><em> </em>of the event. It allows events to evolve over time (adding or changing fields) without breaking consumers</li><li><strong>type — </strong>the event type that uniquely describes what happened (e.g., PaymentSuccess)</li></ul><p>You may be wondering why the hell we need a <em>type </em>in metadata, if the event’s name should be sufficient 😅. Yes, event names are useful, but having a type in the metadata allows us to ease the refactoring (event renaming does not require changes in all consumers).</p><p>Similarly to eventId, it is also recommended to use a string instead of an enum for type. As it would allow for avoiding frequent changes to shared libraries, and reduce coupling between services.</p><p>You can also include other fields in metadata, like publishedBy, createdAt, <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26#506a">correlationId</a>, <a href="https://medium.com/@iamprovidence/multitenancy-in-action-f4b110861314">tenantId</a>, and so on.</p><p>Metadata makes events self-describing. It allows consumers to process messages safely, trace their origin, avoid duplicates, and remain independent.</p><p><strong>— payload</strong></p><p>The <strong>payload</strong> is an optional part of an event that contains business data specific to that event.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*1aXsBqjmaZ-JyJiWaftatA.png" /></figure><p>There is no universal rule for what must be inside it. A common question is: <em>what data should be included?</em> How can a publishing service know in advance what data other consumers might need?</p><p>In theory, the publisher and consumers could <a href="https://medium.com/@iamprovidence/relationships-between-bounded-contexts-in-ddd-ce5cfe3aaa04#f236">agree</a> on the exact data structure. And this is often an acceptable option if you have control over the producer and the consumer.</p><p>However, this creates tight coupling, which is especially undesirable in large systems or when events are exposed for external consumption.</p><p>Therefore, it is better when the publisher <strong>does not know</strong> how the data will be used, so publishers and consumers remain isolated and decoupled.</p><p>Choosing the right level of data granularity is crucial, so we’ll explore this in the next section.</p><h4>Event Granularity</h4><p>In EDA, it is important to choose how detailed or coarse your events are — how much information each event contains and how frequently they are emitted. Selecting the right granularity is crucial for clarity, performance, and maintainability.</p><p>For some events, no additional data is needed — their name or type is self-explanatory, so only the entity identifier is provided. The approach is called <strong>Event Notification.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/957/1*J9vaz1IqVflSiRz7FTNEqA.png" /></figure><p>While it may work for some events, others like UserAgeChanged, will cause consumers to fetch additional missing data explicitly via an API.</p><p>Small messages are lightweight, faster to process, minimize bandwidth, and easier for consumers to subscribe to. However, it is less scalable, slower due to additional queries, and creates coupling.</p><p>An alternative would be the <strong>Event-Carried State Transfer </strong>pattern, where the event carries all relevant state information related to the entity.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/969/1*Oe1HtxZKysjcldfpT5Mf5A.png" /></figure><p>This method decreases coupling, scales more efficiently because consumers can handle the event without sending additional requests or putting extra load on the source.</p><p>However, larger messages increase network load, make serialization slower, can lead to data inconsistencies (as you have multiple sources of truth), and potentially can lead to data structure coupling.</p><p>The final choice is to use a <strong>document message</strong> and convey the entire snapshot of the entity state<strong>.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/1*zy2TLKSOWpIKS-q0M4pFFA.png" /></figure><p>Document events are more challenging to listen to for specific changes, often requiring consumers to calculate deltas manually, and are less efficient in terms of memory and network bandwidth. However, they are easier to publish and eliminate the need for data fetching.</p><p>So, which option to choose?</p><p>The choice between those approaches depends on factors such as performance, memory usage, data ownership and consistency, consumer flexibility, coupling, and other requirements.</p><p>Publishing too many small events can overload the system and make the overall flow hard to understand — especially when you need to roll back changes. On the other hand, grouping too much into a single event can trigger unnecessary processing by consumers.</p><p>First and foremost, events exist because they are important for your domain. You should avoid modeling technical changes; focus on business-relevant transitions. An event should describe something that matters to a business stakeholder or domain expert.</p><p>Aim for balance by thinking about the impact of each event and whether consumers must dig into the payload just to decide how to react.</p><h4>Event Versioning</h4><p>Your application’s <a href="https://medium.com/@iamprovidence/55f35ab38f45/">business logic</a> changes over time. You may add fields, rename concepts, split/merge entities, etc.</p><p>However, old events that are already serialized, published, and stored in the broker still exist and will be consumed with outdated structures.</p><p>Therefore, you would have to manage event changes somehow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i5li-WWvBhomudFC1SqRCw.png" /></figure><p>First, keep in mind that some changes are <em>backward-compatible </em>— for example, adding a new property. If consumers don’t need this new property, they can simply ignore it.</p><p>However, if the change is significant (for example, renaming a property or removing important data), consumers will need to adapt. In such cases, you will likely introduce a version, either in the event name (e.g., UserNameChangedV1, UserNameChangedV2) or in its metadata. Consumers can support multiple versions in parallel and migrate safely as the event contract changes.</p><p>It’s also important to continue publishing older event versions until all consumers have migrated.</p><h4>Event Reuse</h4><p>When you have a publisher and subscribers sharing events, the first instinct is often to reduce code duplication by placing event definitions in some shared location.</p><p>You generally have a few options:</p><ul><li>Shared library (or a NuGet package)<strong> </strong>— this works well, but only if both producers and consumers are written in the same programming language</li><li>Interface definition language (IDL) — you define an event schema, and each team uses tooling to generate event classes in their own language (similar to how Swagger/OpenAPI generates code from an API specification)</li></ul><pre>struct UserEvent {<br>    1: i32 userId,<br>    2: string name,<br>}</pre><ul><li>Event duplication — sometimes duplicating code is perfectly fine 🙃. If events are pure DTOs with no logic, duplication avoids stamp coupling and allows consumers to have different schemas, deserializing only the fields they actually need</li></ul><h3><strong>Event Source (Producer/Publisher/Emitter)</strong></h3><p>The next component we are going to discuss is an <strong>event source.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xC1qKAsbA7cw3nFDe_DIQA.png" /></figure><p>It is responsible for creating and publishing events when something meaningful happens in the system.</p><pre>public class UserService : IUserService<br>{<br>    private readonly IEventProducer _eventProducer;<br><br>    public UserService(IEventProducer eventProducer)<br>    {<br>        _eventProducer = eventProducer;<br>    }<br><br>    public async Task CreateUser(User user)<br>    {<br>        // business logic<br>                  <br>                            . . .<br><br>        <br>        // publish events<br>        var event = new UserCreatedEvent(user.Id, user.Name);<br>        await _eventProducer.Publish(@event);<br>    }<br>}</pre><p>The emitter does not care who will handle the event or what will happen next — it simply reports a fact and moves on.</p><p>This keeps the producer loosely coupled from consumers and helps the system evolve independently.</p><p>Let’s see how an event producer differs from the traditional REST approach.</p><h4><strong>Uncertainty in Event Delivery</strong></h4><p>With REST APIs, we send a request and can determine how successful it was based on the response. In the Event Driven Architecture, things become more complicated.</p><p>When we publish an event, there is no guarantee that it will not be lost.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SojeLsYe35qeMBdgljSMOw.png" /></figure><p>For example, the event may be delivered to the message broker but not yet consumed. If the message broker fails, the message could be lost. From the service’s perspective, the delivery operation appears successful.</p><p>Even if the event is successfully delivered, we still have no way of knowing whether a consumer successfully processed it or if the order was right.</p><p>There could be no subscriber at all.</p><p>Moreover, we cannot know in advance <em>when</em> the event will be consumed.</p><p>This significantly complicates the overall application logic. If, for any reason, the consumer fails to receive or process this event, the system may end up in an inconsistent state. This responsibility is usually delegated to the message broker, which we’ll cover in the next sections.</p><h4>Outbox Pattern</h4><p>Ideally, all parts of your infrastructure (databases, message brokers, and other dependencies) should always be up and running. In practice, this is hard to achieve, and therefore, you should always try making your code robust and prepared for failure.</p><p>Consider a typical scenario where the message broker is unavailable. Your service successfully persists data in the database, but fails to publish the corresponding event.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2o3fDTZSm2TXQpkH6sUHvQ.png" /></figure><p>As a result, an inconsistency appears between the event publisher and its consumers: the state has changed, but no event was emitted to notify other services.</p><p>This problem is commonly addressed using the <a href="https://medium.com/@iamprovidence/outbox-pattern-01f773451f0f"><strong>Outbox pattern</strong></a>. Instead of publishing events after an operation has completed, the service stores them in the database during the transaction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q81wA9euc7bbCNch__nOfA.png" /></figure><p>Later, a <a href="https://medium.com/@iamprovidence/the-good-the-bad-and-the-ugly-ihostedservice-8be82d063584">background process</a> reads these stored events and publishes them to the message broker, ensuring reliable event delivery.</p><h4><strong>Batching Events</strong></h4><p>Sending (or persisting) messages one by one is highly inefficient. Moreover, since we do not expect an immediate response and communication is asynchronous, there is no real need to process each message individually.</p><p>To address this, the <strong>Message Sequence pattern</strong> is used. Messages are typically grouped into sequences (batches) and sent together, which improves throughput and reduces the overhead associated with message delivery.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9D9OQF3lXb-akOrU4-ENHg.png" /></figure><h4>Event Sourcing</h4><p>When applying the Outbox pattern, an application performs two operations: persisting state and publishing events. These operations must stay in sync because they represent the same state transition. But what if you could rely only on events and compute the current application state from them? That is the idea of <a href="https://medium.com/@iamprovidence/event-sourcing-7dc8bb63db7c"><strong>Event Sourcing</strong></a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DD6Eg2vg4Mpn9HbCj10Ehw.png" /></figure><p>Event-Driven Architecture and Event Sourcing are frequently used together because they complement each other naturally.</p><p>In EDA, events represent facts about what has already happened and are used to notify other parts of the system.</p><p>Event Sourcing takes this idea further by making those same events the source of truth for application state. Instead of storing only the current state, the system persists a sequence of domain events and rebuilds the state by replaying them.</p><p>This aligns perfectly with EDA, where events are already being produced and consumed.</p><blockquote><em>⚠️ By the way ⚠️<br>When using a broker like Kafka to store events instead of a traditional database, it can effectively serve as both a data store and a message broker.</em></blockquote><h4>CQRS</h4><p>If you opted in for Event Sourcing, it’s also a good idea to consider <a href="https://medium.com/@iamprovidence/cqrs-ba4545440a3c">CQRS</a>, especially when your service is frequently queried.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/571/1*80Gdon0nyvf6WjoDKJjzww.png" /></figure><p>Implementing CQRS can help by improving query performance and reducing the need to constantly recalculate state from events.</p><h3><strong>Event Broker</strong></h3><p>After the event is created and published, it is sent to the event broker, a component through which the source and the listener exchange events</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E_9IA_qwBd5wFqBAhkQ7TQ.png" /></figure><p>Sometimes developers wonder why we even need this component instead of directly sending events to each listener. This would result in tight coupling, making the system harder to maintain and scale.</p><p>Additionaly, handling failures or retries would become the producer’s responsibility, but as we are following the Single Responsibility principle, it&#39;s better to delegate the work to someone else 😅.</p><p>Technically, you could implement your own broker by storing events in a database and sending them to consumers, similar to a webhook mechanism. However, event-driven systems often have complex architectures and require advanced algorithms for efficient processing, routing, and delivery of events. Instead of building this from scratch, it is common practice to use existing, battle-tested solutions or proven architectural patterns.</p><h4><strong>Canonical Data Model</strong></h4><p>After the event is published, it is likely to be stored. However, which format should we use?</p><p>Technically, it’s possible to use one format for writing and another for reading, but this makes the system harder to understand and integrate.</p><p>The <strong>Canonical Data Model</strong> pattern addresses this by proposing a single, standardized data format for all participants in the message exchange.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/933/1*2Owli99u0OCdx9sV2-JdLQ.png" /></figure><p>Think of how we handle REST APIs: we usually pick a popular format like JSON and use HTTP as the transport protocol. This works well because almost every programming language has an HTTP client and libraries for creating and parsing JSON documents. It allows new microservices to be written in any language and run on any platform.</p><p>While being widely understood, JSON is not the most compact or performance-efficient format. There are others, like Protobuf<strong> </strong>or others. Just ensure it is platform-independent.</p><h4>Topologies</h4><p>In the <strong>Broker topology (</strong>Choreography<strong>)</strong>, components publish events to a shared event broker, and other components decide independently whether to react to them or ignore them.</p><p>This approach works well when the event flow is relatively simple, but the lack of coordination makes <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#ad20">multi-step business processes</a> harder to manage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/895/1*17x_f1D-1xVNiP2wJ5vq-A.png" /></figure><p>Alternatively to this approach, we have the <strong>Mediator topology</strong> (Orchestration). Instead of broadcasting events, components publish events as commands to a specific mediator component.</p><p>The mediator manages process state, error handling, and retries, providing better control and consistency across distributed operations.</p><p>The trade-off is tighter coupling and the risk of the mediator becoming a bottleneck or single point of failure if poorly designed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/883/1*kFDeZteLDaNEGs1CuN-cSQ.png" /></figure><blockquote><em>⚠️ By the way ⚠️</em></blockquote><blockquote><em>Typically, each domain has its own mediator, which helps reduce the risk of a single point of failure.</em></blockquote><h4>Communication Patterns</h4><p>With the <strong>Event</strong> <strong>Bus</strong> pattern, participants are completely decoupled from each other. Services don’t need to know about each other, only about the bus. Consumers can act upon or filter out unwanted events, join or leave at any time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/969/1*d9njHAb-NK5VpaGrhlk0nw.png" /></figure><p>If a message must have exactly one recipient, the <strong>Point-to-Point</strong> pattern is recommended, where both parties know about the channel. Even if multiple consumers are connected to the same message channel, only one of them will receive a given message. This pattern is usually implemented using <em>queues</em> and is more suitable for <em>commands</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UiTUH1gHvWVnswqni0NBcQ.png" /></figure><p>If a message should be delivered to all consumers, the <strong>Publish/Subscribe</strong> pattern should be used. This is typically implemented using <em>topics</em> and is ideal for <em>events</em>, where multiple consumers may need to react to the same events.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0W_XXoMZVHhK6tOxGAE1DQ.png" /></figure><h4><strong>Datatype Channel</strong></h4><p>The <strong>Datatype Channel</strong> pattern promotes the idea of using <em>one channel per event type</em>.</p><p>In this approach, both the sender and the receiver know the channel name in advance and, based on it, understand what kind of data is published there and in which format (e.g UserCreatedEvent is published in UserCreatedEventChannel).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tGBRmz34g0V9WnozQ6i7Xw.png" /></figure><p>As a result, messages do not need to include extra metadata describing their structure or having additional logic to filter, transform or deserialize to correct types when processing the events.</p><h4>Message Consumption Models</h4><p>Messages can be delivered in two ways: push-based, where consumers receive messages automatically, and pull-based, where consumers read stored messages when ready.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*vukRLfO9kugBeUY_.png" /></figure><p>In the <strong>publish/subscribe</strong> model, the messaging infrastructure manages subscriptions. When an event is published, it is pushed to all registered subscribers. It is simple, provides low latency, and is good for real-time reactions, but you cannot replay events, or you can even <em>miss some</em> if subscribed later.</p><p>In the <strong>event streaming</strong> model, events are appended to the list. Consumers read events from this stream. It is harder to implement, requires more storage space, but you can make sure events will be handled <em>even when the consumer disconnects</em>, you can easily reply to events, and <a href="https://medium.com/@iamprovidence/queue-based-load-leveling-pattern-8aa7c31d0770">handle them at your own pace</a>.</p><h4><strong>Broker Reliability</strong></h4><p>A message broker can handle events in different ways: it may persist them temporarily in memory (for example, RedisMQ) or store them in <strong><em>durable</em></strong><em> </em>storage, ensuring that messages remain in the system even after broker restarts or shutdowns.</p><p>The choice depends on balancing these factors:</p><ul><li>durability: Do I need a history of incoming messages (pull model), or messages are deleted after they are processed (push model)?</li><li><a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#1771">scalability</a>: Is the incoming message processed by another application? By multiple consumers?</li><li>latency: How crucial is the time it takes to store and retrieve the message?</li><li>resilience: Can I afford to lose all persisted inbox messages when the application goes down?</li></ul><p>Often, in a large distributed system, it’s important that messages are not lost— even if the broker was temporarily down. Therefore, this requires the messaging system to have reliable storage for messages.</p><p>The <strong>Guaranteed Delivery</strong> pattern ensures that a message <em>is stored in the system </em>and not removed until all consumers have processed it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/609/1*LJ-2DHVR4Bhnu3Ow9qHcNA.png" /></figure><p>Additionaly, it is also necessary to track the state of each consumer — what messages they have already read and which are still pending. This prevents missed messages if a consumer becomes overloaded or temporarily unavailable.</p><p>The <strong>Durable Subscriber</strong> pattern addresses this, requiring each consumer to have a unique identifier so it can be distinguished from others.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/763/1*VFDP4RZcjNNqUZU_j-hFog.png" /></figure><h4><strong>Message-Delivery Semantics</strong></h4><p>Depending on what matters more to your business — guaranteed delivery or single processing — different <strong>message delivery semantics</strong> can be applied.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*99Q2wlwkvDpzfO0b.png" /></figure><p>Notice, exactly-once seems like an ideal option, but achieving it is nearly impossible because a message may be processed multiple times — especially after failures — so consumers must be designed to handle reprocessing safely using the <strong>Idempotent Receiver</strong> pattern to prevent duplicate side effects.</p><h4><strong>Dead Letter Channel</strong></h4><p>If an error occurs while processing a message due to its invalid content, the <strong>Dead Letter Channel</strong> pattern suggests routing the “poisoned” message to a special channel for further analysis and troubleshooting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nva4BGr0wdA97w_VTFlUHw.png" /></figure><p>Note, this pattern applies both to push-based topics and pull-based channels, where messages can be skipped or deferred during processing.</p><h3>Event L<strong>istener</strong></h3><p>The final component we are going to discuss is <strong>event listeners</strong>. They are responsible for receiving events from the event broker and reacting to them by executing some business logic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bwTUHF3W5eZ_nALBRBdW4Q.png" /></figure><p>An event consumer subscribes to one or more event types and is triggered whenever a relevant event occurs.</p><pre>public class UserCreatedEventHandler : IEventHandler&lt;UserCreatedEvent&gt;<br>{<br>    public async Task Handle(UserCreatedEvent @event)<br>    {<br>        // react to the event<br>        // for example: send email, update local record in DB, etc...<br>    }<br>}</pre><p>The consumer focuses only on what to do when the event happens, not on <em>who</em> produced the event.</p><p><em>Multiple consumers</em> can react to the same event independently, each implementing its own behavior.</p><p>This loose coupling is one of the key strengths of Event-Driven Architecture and is a major difference compared to traditional REST-based communication, where services must explicitly call each other and coordinate execution.</p><h4>Message Consumption Models</h4><p>We already discussed this from the broker’s perspective, but let’s revisit the same terminology from the consumer’s.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*vukRLfO9kugBeUY_.png" /></figure><p>When the consumer subscribes to messages and provides <em>an asynchronous callback</em> method that is automatically invoked whenever a message arrives, it is the <strong>Event-Driven Consumer </strong>pattern.</p><p>If a consumer is ready to receive messages and <em>synchronously</em> <em>waits</em> for new messages, it is known as the <strong>Polling Consumer</strong> pattern.</p><h4><strong>Asynchronous Processing</strong></h4><p>In an event-driven architecture, producers and consumers are decoupled, allowing them to work independently of each other’s performance.</p><p>This is especially useful when consumers are slower than producers, as messages can be buffered and processed at their own pace without blocking the producer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Uga5PMSp5LK2W_MnpoArBg.png" /></figure><p>From a performance and scalability perspective, asynchronous event processing is a major advantage. However, it also introduces additional complexity in managing cross-service communication, including logging, distributed tracing, error handling, and distributed transactions.</p><h4>Competing Consumers Pattern</h4><p>Processing messages one by one is simple, but it is not the fastest approach. And what do we do to improve throughput? We scale, vertically or horizontally.</p><p>Let’s say we add more instances of the same server. It means messages can be processed in parallel. However, when multiple consumers compete for messages in the same message channel, multiple challenges arise, like handling messages only once, proper message ordering, load balancing, fault tolerance, etc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/938/1*Uw9A2_W8uOJ1QzufV2ANMA.png" /></figure><blockquote><em>⚠️ By the way ⚠️</em></blockquote><blockquote><em>Although multiple consumers are involved, this is not the same as the Publish-Subscribe pattern. With Competing Consumers, each message is </em>handled once<em>, whereas in Pub/Sub, every consumer receives a message.</em></blockquote><h4>Eventual Consistency</h4><p>As components are decoupled and communicate asynchronously, it is quite hard to achieve strong consistency.</p><p>When data is distributed across components and updated via events, changes don’t propagate instantly — they require time to reach all parts of the system. Users may see outdated data. This results in <strong>eventual consistency</strong>.<strong> </strong>It<strong> </strong>means that data will be updated… eventually 😁. But not immediately.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UzWT_0ub1DxO2b00baYtyQ.png" /></figure><p>Of course, if both the publisher and the consumer are part of the same process, they can be grouped under a single transactional scope, ensuring atomic execution. However, this effectively collapses the system back into a tightly coupled, synchronous design and defeats the core purpose of EDA.</p><p>As a result, most EDA systems typically favor eventual consistency, accepting temporary inconsistencies in exchange for scalability, resilience, and loose coupling.</p><h4>Client Acknowledgement Mode</h4><p>A message can be acknowledged as soon as it is received or processed, but this approach can lead to serious issues.</p><p>One common problem is consumer failure — either software or hardware — during message processing. If the message is marked as acknowledged in the broker before processing completes, the message is effectively lost and will never be handled.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/773/1*jVuTEpDHKVppY1_JBmirEw.png" /></figure><p>To avoid this, consumers can use manual acknowledgements, confirming message consumption only after successful processing.</p><h4><strong>Idempotency</strong></h4><blockquote><strong><em>Idempotency</em></strong><em> is a property of an operation, which guarantees that performing the operation multiple times will yield the same result as if it was executed only once</em></blockquote><p>Or in plain English: if an event consumer, due to a system bug, receives the same “charge money” event ten times, the payment will be processed only once.</p><p>Idempotency is a critical concept in event-driven architectures because message delivery is not guaranteed to happen exactly once.</p><p>Picture this: a consumer starts processing an event, updates the database, but fails before acknowledging it; the message broker retries delivering the same event. As a result, a consumer receives duplicate messages.</p><p>To prevent this, consumers should be designed so that processing the same event multiple times produces the same outcome as processing it once. This is commonly achieved by tracking processed event identifiers, using idempotency keys, or relying on database constraints.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/749/1*B6WumBVxoByXV10bDleKow.png" /></figure><p>Without idempotent handling, retries can lead to inconsistent state, duplicated side effects, or corrupted data.</p><h4>Compensating Transactions</h4><p>Error handling becomes especially challenging in distributed systems due to the lack of atomicity. The more services that participate in a single business transaction, the harder it is to guarantee that all steps either succeed or fail together.</p><p>For instance, a service executes an action successfully, but later the system discovers that the next consumer fails. In such cases, it must reverse the operation by publishing a compensating event that undoes the previous action.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*0ob4ioTPKkjgkgkF.png" /></figure><p>The difficulty is that these steps occur asynchronously. <strong>Compensating transactions</strong> are rarely a simple rollback of the original operation. By the time compensation runs, the system state may have already changed due to other transactions, which makes reliable recovery even more complex.</p><h4>CQRS</h4><p>CQRS is not limited to the producer side and can be effectively applied to consumers, too.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*ZSSB7xbjSk_GkZC3y4lCZQ.png" /></figure><p>Since CQRS operates in an asynchronous world, commands can be delivered to consumers via a queue, processed independently, and the resulting outcomes can be published back to the event bus.</p><h3>Summary</h3><p>As you see, compared to REST, Event Driven Architecture does not have well-established conventions.</p><p>Which event (or events) should be emitted? How should event classes be named, and what data should they contain? How can we be sure that the provided information is sufficient for other services to handle the event correctly?</p><p>Fortunately, we can rely on multiple practices and patterns 🙃.</p><ul><li>The core idea of Event-Driven Architecture (EDA) is to produce an event on every state change. Something <strong>happens</strong> → an <strong>event</strong> is created</li><li>Despite its conceptual simplicity, this architecture works regardless of your scale: with classes, modules, or distributed microservices</li><li>Many projects combine Event-Driven Architecture with others like microservices, modular monoliths, etc</li><li>EDA does not mean microservices, but often complete them</li><li>Use this architecture when multiple subsystems need to handle the same events, and you want to decouple producers from consumers to scale and evolve them independently</li><li>The clear benefits are that it decouples producers and consumers, improves scalability, resilience, and performance through asynchronous processing and horizontal scaling</li><li>The challenges it brings are increased infrastructure costs and operational complexity, complex error handling, data inconsistency, event ordering, idempotency, and data loss</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XcFwQOJkJdlFosVy3rnxMw.png" /></figure><p>Pick the approach that best suits the complexity and requirements of your domain 😉</p><p>Give this article a clap or a few if you like it 👏<br>You can support me with the link below ☕️<br>Don’t forget to follow to receive more ✅<br>Out the door, dinosaur 🦖</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9cc85e8c3795" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Event-Driven Programming]]></title>
            <link>https://medium.com/@iamprovidence/event-driven-programming-988008ccf2be?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/988008ccf2be</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Sun, 07 Jun 2026 10:01:03 GMT</pubDate>
            <atom:updated>2026-06-07T10:01:03.098Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DQvcQqFtf7nhYFVnYebhng.png" /></figure><p>Imagine you are sitting in a room waiting for the phone to ring. You don’t constantly check the phone every second, asking: <em>“Did someone call? Did someone call now?”. </em>You&#39;re just doing your work. And when the phone rings, you react.</p><p>It turns out many real-life processes work exactly this way. Moreover, this is how many applications work, too. This way of writing programs can even lead to a completely different programming style: instead of constantly checking whether something happened, a program can simply wait for events and react when they occur.</p><p>This concept is known as Event-Driven Programming. In this article, we will explore what this paradigm is all about: the different ways components communicate — such as Observer, Mediator, and Event Bus — and common techniques for handling events, including debouncing vs throttling, buffering, batch processing, and event aggregation.</p><p>In case you are interested in other programming paradigms, look here:</p><ul><li><a href="https://medium.com/@iamprovidence/6e8eb85f97f0">Structured Programming</a></li><li><a href="https://medium.com/@iamprovidence/object-oriented-programming-078c2cb21b22">Object-Oriented Programming</a></li><li><a href="https://medium.com/@iamprovidence/aspect-oriented-programming-91bc43929dc8">Aspect-Oriented Programming</a></li><li><a href="https://medium.com/@iamprovidence/functional-programming-3c5b9ad99554">Functional Programming</a></li><li><strong>Event-Driven Programming</strong></li><li><a href="https://medium.com/@iamprovidence/reactive-programming-eb6ca09f5c35">Reactive Programming</a></li></ul><p>Are you ready?😃 Let us begin.</p><h3>Event-Driven Programming</h3><p>The popularity of event-driven architectures comes from a simple fact: the imperative paradigm (executing commands step by step) doesn’t reflect how the real world works.</p><p>While most programs execute code in a linear flow, each step is explicitly invoked after the next one:</p><pre>var order = CreateOrder();<br><br>SendNotification(order);<br>ChargePayment(order);<br>LogOrder(order);</pre><p>In the real world (and in many systems too), actions are triggered as a result of other actions.</p><pre>var onEvent = _eventBroker.When&lt;OrderCreated&gt;();<br><br>onEvent.Execute((o) =&gt; SendNotification(o));<br>onEvent.Execute((o) =&gt; ChargePayment(o));<br>onEvent.Execute((o) =&gt; LogOrder(o));</pre><p>The simple concept of publishing events and responding with callbacks can serve as the foundation for an entire programming paradigm:</p><blockquote><strong><em>Event-Driven Programming (EDP)</em></strong><em> is a programming paradigm in which operations are triggered by changes in state of other objects.</em></blockquote><p>Many things can trigger events: a user clicking a button, a payment succeeding, <a href="https://medium.com/@iamprovidence/too-many-collections-in-c-a6a23b636119#35a0">an object being added to a collection</a> — essentially, anything that changes the system’s state.</p><p>Once you start noticing it, event-driven programming appears everywhere 😁.</p><p>However, applying an event-driven approach can also make the program harder to reason about. Instead of a clear sequence of function calls, behavior is spread across many event handlers, which can complicate debugging, make it harder to trace the flow of execution, and increase the cognitive load for developers.</p><p>Let’s take a closer look and see what it means in practice.</p><h3>Core Concepts</h3><p>At its core, event-driven programming models systems around the occurrence of events rather than direct method calls.</p><p>Something happens → An event is published → Callbacks are triggered.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8MkFxAM9hPvFrYW2NV4w1Q.png" /></figure><p>In this paradigm, you can highlight the following components:</p><ul><li><strong>event</strong> — a fact that something happened (UserRegisteredEvent, etc)</li><li><strong>event source (producer/publisher/emitter) </strong>— someone who produces an event</li><li><strong>event listener</strong> <strong>(consumer/subscriber/sink)</strong> — someone who reacts to the event</li><li><strong>event broker (event loop)</strong> — a tool through which the source and the listener exchange events</li></ul><h3>Event Communication Patterns</h3><p>In event-driven systems, components need a way to produce and react to events. Several patterns organize this communication, each providing a different level of decoupling and control.</p><p>Let’s look at some common approaches.</p><h4>Observer</h4><p>Imagine you build a system that sends notifications when an order is created. Multiple parts of the system may be interested in this event — one component may send an email, another update the <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84">cache</a>, and a third one writes <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logs</a>.</p><p>A naive approach would be to call all these components directly from the order service.</p><pre>public class OrderService<br>{<br>                .  .  .<br>    public void CreateOrder(string orderId)<br>    {<br>        var order = new Order();<br><br>        _emailService.SendEmail(order);<br>        _cacheService.UpdateCache(order);<br>        _logger.AutidLog(order);<br>    }<br>}</pre><p>But this <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d">tightly couples</a> the order creation logic with every feature that needs to react to it. As the system grows, the code becomes harder to maintain.</p><p>To solve this, we can apply the <strong>Observer</strong> pattern. Publishes an event so anyone can subscribe to it and react when it happens.</p><pre>public class OrderService<br>{<br>    public event Action&lt;Order&gt; OrderCreated;<br><br>    public void CreateOrder(string orderId)<br>    {<br>        var order = new Order();<br>        OrderCreated?.Invoke(order);<br>    }<br>}<br><br>public class EmailNotifier<br>{<br>    public void OnOrderCreated(Order order)<br>    {<br>                .  .  .<br>    }<br>}<br><br>------------------------------------------------------------------------<br><br>orderService.OrderCreated += _emailNotifier.OnOrderCreated;<br>orderService.OrderCreated += _cacheService.OnOrderCreated;<br>orderService.OrderCreated += _logger.OnOrderCreated;</pre><p>This way, the event producer does not need to know who is listening. New functionality can be added by simply creating a new event handler without modifying the existing code.</p><p>This pattern is widely used in UI frameworks like WinForms, HTML + JavaScript, or any <a href="https://medium.com/@iamprovidence/mvc-vs-mvp-vs-mvvm-with-c-examples-8013745e3c4c#b744">MVP model</a>, where the UI control and its event handler are naturally tied together. However, in the backend API server, where component state is renewed per request, maintaining persistent subscriptions across requests is difficult.</p><h4><strong>Mediator</strong></h4><p>Imagine you have multiple services in a system that need to communicate with each other in both directions. If each service directly triggers the others, you’d have multiple subscriptions and dependencies everywhere, and the system quickly becomes <a href="https://medium.com/@iamprovidence/f19688afe006/">tangled</a> and hard to maintain.</p><p>To address this, we can use the <strong>Mediator</strong> pattern. With it, all communication is centralized — services interact with each other indirectly through the mediator.</p><pre>public interface IEventMediator<br>{<br>    event Action&lt;Order&gt; OrderCreated;<br>    void PublishOrderCreatedEvent(Order order);<br><br>    event Action&lt;Order&gt; OrderUpdated;<br>    void PublishOrderUpdatedEvent(Order order);<br>}<br><br>public class OrderService(IEventMediator _mediator)<br>{<br>    public void CreateOrder(string orderId)<br>    {<br>        var order = new Order(orderId);<br>        _mediator.PublishOrderCreatedEvent(order);<br>    }<br>}<br><br>________________________________________________________________________<br><br>_mediator.OrderCreated += _emailNotifier.OnOrderCreated;<br>_mediator.OrderCreated += _cacheService.OnOrderCreated;<br>_mediator.OrderCreated += _logger.OnOrderCreated;</pre><p>Here, email, <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84">cache</a>, or <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logging</a> services do not know about the OrderService . The mediator handles all communication.</p><p>The <a href="https://medium.com/@iamprovidence/di-mistakes-you-should-not-be-doing-6d1dcaa80d13#ca35">always-on</a> Mediator works well in backend systems, where components are often short-lived or stateless, because it is durable, centralizes interactions, and avoids the problem of maintaining persistent subscriptions.</p><h4>Bus</h4><p>If you want to take decoupling one step further, just like the mediator, you can implement an <strong>Event Bus</strong> to coordinate communication between components.</p><p>With a mediator, there is a central object that knows how components interact. But an event bus removes even that knowledge. Components simply publish events, and any component that is interested can subscribe to them.</p><pre>interface IEvent { }<br><br>class OrderCreatedEvent : IEvent { . . . }<br>class OrderUpdatedEvent : IEvent { . . . }<br><br>public interface IEventBus<br>{<br>    void Subscribe&lt;TEvent&gt;(Action&lt;TEvent&gt; handler);<br>    void Publish(IEvent e);<br>}<br><br>----------------------------------------------------------------------<br><br>// a component can publish an event:<br>_eventBus.Publish(new OrderCreatedEvent());<br><br>// and other components can subscribe to it:<br>_eventBus.Subscribe&lt;OrderCreatedEvent&gt;(e =&gt;<br>{<br>    // send email<br>});<br><br>_eventBus.Subscribe&lt;OrderCreatedEvent&gt;(e =&gt;<br>{<br>    // update metrics<br>});</pre><p>However, this flexibility comes with a trade-off. With an event bus, it becomes harder to track navigation in the codebase. Since there are no direct references between components, IDE tools cannot easily show the full flow of events. Understanding who publishes and who handles an event often requires additional investigation and clean code conventions.</p><h3>Event Processing Patterns</h3><p>With these core concepts in place, we can look at common patterns used in event-driven systems. These patterns solve recurring problems such as how events are delivered, how event flow is managed, and how event frequency is controlled.</p><h4><strong>Debouncing</strong></h4><p>Imagine you implement a typical search bar. Every time a user types a character, an event fires.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/682/1*xNRSWoimMEv6Efdldz1iLQ.png" /></figure><p>If we send a request to the API on every keystroke and rerender the results immediately, it becomes wasteful and can overload the system.</p><p>To optimize this, we can apply <strong>debouncing</strong>. It will <em>trigger one event</em> only <em>when </em>the user has <em>stopped</em> typing in the search bar <em>for at least 500 milliseconds</em>.</p><p>There are many similar scenarios where events fire very frequently, but the work they trigger is expensive — typing in an input field, resizing a window (where you could have thousand events per second), applying many filters, etc.</p><p>In such cases, debouncing delays execution of an event handler until a certain amount of time has passed without new events.</p><h4>Throttling</h4><p>Imagine you design a fighting game. Each time a user presses a button, it triggers an event.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WPL7nIU54tFUogP4g7lzXg.png" /></figure><p>Usually, players press buttons very rapidly to perform an attack. For example, 10 times within a second, while the character in the game is only able to perform one attack.</p><p>In such cases, it makes sense to <strong>throttle</strong> the events. Throttling ensures that the event is triggered once within a certain period.</p><p>There are many other scenarios where this technique is useful — such as rapid mouse clicks, users clicking a button multiple times instead of once, continuous scrolling, or <a href="https://medium.com/@iamprovidence/rate-limiting-8d1aa513f40a">rate limiting</a>.</p><h4>Buffering</h4><p>Both debouncing and throttling reject excessive events to execute an operation once in a certain period.</p><p>However, sometimes you need to handle all the events. For example, your system processes images uploaded by users. Each time a user uploads an image, an event is generated to start processing it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iEWn4tw3k6rraIZugGdh4g.png" /></figure><p>The thing is that processing an image takes time, and multiple users may upload images faster than the system can handle them.</p><p>In such cases, it makes sense to <strong>buffer</strong> the events. Instead of processing each event immediately, the system places events in a queue, allowing consumers to <a href="https://medium.com/@iamprovidence/queue-based-load-leveling-pattern-8aa7c31d0770">handle them at their own pace</a>.</p><p>This approach ensures that consumers can handle the workload at a sustainable rate. Buffering is commonly used in systems where the event producer is faster than the consumer, such as video processing, order processing, or background task execution.</p><h4>Batch Processing</h4><p>Let’s say your component writes <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logs</a> from all the events. Every time something happens, a log event is generated and persisted in a database.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P9VlCBg0sI_U4mvrfrgJbA.png" /></figure><p>If the system writes every log entry to the storage immediately, it may perform many small and expensive operations.</p><p>In such cases, it makes sense to apply <a href="https://medium.com/@iamprovidence/you-should-not-care-about-performance-and-here-is-why-42b327597f6d#cee9"><strong>batch processing</strong></a>. Instead of processing each event right away, the system handles a group of them.</p><p>For example, the application may collect log events for a few seconds, buffer them to a queue, and then write them to the database in a one batch.</p><p>This approach is useful to reduce the large number of operations and improve performance when many events occur within a short period of time.</p><h4>Event Aggregation / Coalescing</h4><p>It is common for a website analytics system to track how many users are viewing a page in real time. Every user action — like loading a page or clicking a link — generates an event.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jMxS3V8HzQHY2PStHG-5cQ.png" /></figure><p>If thousands of users interact at once, processing each event individually could overload the system.</p><p>In such cases, it makes sense to <strong>aggregate events</strong>. Instead of updating the dashboard for every single event, the system collects events over a short time window — say 1 second — combines them into one event, and then updates the total number of active users at once.</p><p>This approach reduces the number of events, improves performance, and still provides accurate data.</p><h3>Personal Opinion🤔</h3><p>Event-Driven Programming is a powerful paradigm that describes how code inside an application is structured; however, it is rarely used in isolation and is typically combined with other programming approaches when applied, if applied at all 😬.</p><p>Developers are generally more familiar with imperative programming, and most frameworks are designed to encourage that style. Even though, in C#, <a href="https://medium.com/@iamprovidence/reactive-programming-eb6ca09f5c35#b5bd">events</a> exist as a language feature, they are somewhat obscure and rarely used, except in Windows Forms, while modern UI frameworks tend to rely on data binding instead.</p><p>Beign said that EDP still shines in user interfaces, games, monitoring, and other event-driven systems, where it keeps the UI responsive and prevents the application from freezing when actions are triggered.</p><p>It can also be applied in backend systems, particularly in <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">Domain-Driven Design (DDD)</a> or <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">microservices architectures</a>, to improve <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d">loose coupling</a>, scalability, and performance.</p><p>Moreover, this simple code style inspired an entire architecture style, <a href="https://medium.com/@iamprovidence/9cc85e8c3795">Event-Driven Architectures (EDA)</a>, where events coordinate actions across services.</p><h3>Conclusion</h3><p>Event-Driven Programming is a paradigm that focuses on reacting to events rather than executing instructions sequentially.</p><p>It promotes loose coupling and responsiveness. EDP helps manage complexity by <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d">decoupling</a> components, enabling <a href="https://medium.com/@iamprovidence/all-you-need-to-know-about-task-in-c-2dce9e52c0f7">asynchronous</a> processing, improving modularity and reusability, and allowing the system to react and scale more easily.</p><p>Here are some of the main advantages:</p><ul><li><strong>loose coupling</strong>. components don’t need to know about each other. They only communicate through events.</li><li><strong>modularity:</strong> separating event generation from event handling allows components to be developed, <a href="https://medium.com/@iamprovidence/unit-tests-018611590eac">tested</a>, and maintained independently</li><li><strong>scalability</strong><br> — producers don’t wait for consumers to finish, they emit an event and continue their work. <br> — multiple consumers can process the same event in <a href="https://medium.com/@iamprovidence/all-you-need-to-know-about-task-in-c-2dce9e52c0f7">parallel</a>.</li><li><strong>extensibility</strong>. you can add new features without changing existing components.</li></ul><p>💬 Let me know in the comments how often you face the Event-Driven Paradigm?</p><p>👏 Clap if you find something new here</p><p>☕️ Support me if you want</p><p>✅ Follow</p><p>😉 And stay awesome</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=988008ccf2be" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Managing Dependency Chaos in Your Code]]></title>
            <link>https://medium.com/@iamprovidence/managing-dependency-chaos-in-your-code-f19688afe006?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/f19688afe006</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Wed, 27 May 2026 10:01:07 GMT</pubDate>
            <atom:updated>2026-06-14T21:11:55.433Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jc-J76rgEondN-WOml8L6Q.jpeg" /></figure><p>Every developer at least once experienced tangled code where responsibilities are mixed, and even a small change feels risky and painful to make.</p><p>What should be a simple modification turns into a chain reaction. You touch one class, three others break. The kind of complexity comes from uncontrolled dependencies pointing in every direction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_B_uMch6ZRzqvvR91x0Ocw.png" /></figure><p>Uncontrolled dependencies often don’t stay linear — they quietly turn into circles. At first, everything seems fine, but if you start tracing who depends on whom, you suddenly find yourself back where you started. That’s usually a sign the design has drifted, and boundaries are no longer clear.</p><p>Circular dependencies are a problem because they <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d">tightly couple</a> components, making them hard to understand, <a href="https://medium.com/@iamprovidence/unit-tests-018611590eac">test</a>, reuse, or change independently. They break modularity, increase cognitive load, and most importantly, they signal unclear <a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#2d8c">responsibilities</a> and weak architectural <a href="https://medium.com/@iamprovidence/b1fafacf7a90/">boundaries</a> — and that pain only grows as the system <a href="https://medium.com/@iamprovidence/4ddaf42d1a18/">scales</a>.</p><blockquote><em>Therefore, according to </em>The Acyclic Dependencies Principle<em>, the dependency graph must have no cycles. It is preferable for dependencies to be pointed out in the form of an </em>acyclic<em> </em>graph<em> or even a </em>tree<em> (ideally, but unlikely)</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/949/1*yf7hyQfdWXaL9JKnOTyoKQ.png" /></figure><p>The rule applies regardless of <a href="https://medium.com/@iamprovidence/4ddaf42d1a18/">scale</a> — whether you’re dealing with individual classes, projects within a <a href="https://medium.com/@iamprovidence/modular-monolith-6bb4be9232cd">monolith</a>, or even <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">microservices</a>.</p><p>A circular dependency between two small classes is a design smell that can cause runtime issues (especially with <a href="https://medium.com/@iamprovidence/di-mistakes-you-should-not-be-doing-6d1dcaa80d13">DI containers</a>); the same anti-pattern between projects can block builds; and between microservices, it can create tight coupling at the system level. The size changes, but the architectural damage follows the same pattern.</p><p>There is also some good news 🙃. Both the problem and the solution have been known for a long time. For example, traditional <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4">N-layered architecture</a> was designed specifically to enforce clear, one-directional dependencies between components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5DsdlmOm1V3UnVZG2ni7XA.png" /></figure><p>However, as time goes on, the structure becomes unstable and starts to sprawl. A few messy dependencies, and you have cycles. Once they appear, you can’t ignore them. Cycles must be broken to restore clarity and stability. Fortunately, no matter the situation, there are always ways to deal with them 😉.</p><h4>Cycle-Breaking Techniques</h4><p>When you face cycles in a system across multiple components, it may look overwhelming at first.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/457/1*wSEpFWi9JMyPewjS8u9GLQ.png" /></figure><p>However, you don’t need to solve everything at once. Just pick any dependency, and eliminate a cycle there.</p><p>All cycle-breaking strategies come down to one of these moves:</p><ul><li>change direction of dependency (<a href="https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e">Dependency Inversion</a>)</li><li>restructure logic</li><li>decouple the system with <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">events</a></li></ul><p>Each of them changes how responsibilities and dependencies are arranged in the system, so the dependency graph becomes acyclic again.</p><p>Let’s see it in practice, with the simplest case: a cycle formed by two components that directly reference each other.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*VfgegBvDptkp_KVP0hfgUA.png" /></figure><p>In this example, OrderService depends on PaymentService, and PaymentService depends on OrderService. Each service calls the other to complete its work, which creates a circular dependency.</p><pre>public class OrderService(PaymentService _paymentService)<br>{<br>    public void CreateOrder(Order order)<br>    {<br>        _paymentService.ProcessPayment(order.Id);<br>    }<br><br>    public void MarkAsPaid(Guid orderId)<br>    {<br>        Console.WriteLine(&quot;Order marked as paid&quot;);<br>    }<br>}<br><br>public class PaymentService(OrderService _orderService)<br>{<br>    public void ProcessPayment(Guid orderId)<br>    {<br>        Console.WriteLine(&quot;Payment processed&quot;);<br><br>        _orderService.MarkAsPaid(orderId);<br>    }<br>}</pre><p>This is the smallest possible cycle, but if you learn how to break the dependency here, the same techniques can be applied to much larger dependency graphs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/476/1*TqfKQB2yDJj_EbjOd0ASug.png" /></figure><p>In practice, complex cycles are usually just combinations of many small cycles like this.</p><p><strong>— Dependency Inversion</strong></p><p>One way to break a cycle is to revert a dependency on <a href="https://medium.com/@iamprovidence/the-actual-reason-behind-interfaces-9c5aac26eb54">an interface</a> rather than on an implementation, by applying the <a href="https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e">Dependency Inversion principle</a>.</p><p>Instead of PaymentService depending on concrete OrderService, it depends on an abstraction (IOrderUpdater). This way, <em>all the dependencies</em> crossing the <a href="https://medium.com/@iamprovidence/b1fafacf7a90/">boundaries</a> are pointing <em>in the same direction</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*HUn3i4oY9gchAtrRctlr7w.png" /></figure><p>The interface represents the behavior PaymentService actually needs. Instead of referencing the entire OrderService, the dependency is narrowed to just the operation required to update the order state.</p><pre>public class OrderService(PaymentService _paymentService) <br>  : IOrderUpdater // 👈 <br>{<br>    public void CreateOrder(Order order)<br>    {<br>        _paymentService.ProcessPayment(order.Id);<br>    }<br><br>    public void MarkAsPaid(Guid orderId)<br>    {<br>        Console.WriteLine(&quot;Order marked as paid&quot;);<br>    }<br>}<br><br>------------------------------------------------------------------------<br><br>public interface IOrderUpdater<br>{<br>    void MarkAsPaid(Guid orderId);<br>}<br><br>public class PaymentService(IOrderUpdater _orderUpdater) // 👈<br>{<br>    public void ProcessPayment(Guid orderId)<br>    {<br>        Console.WriteLine(&quot;Payment processed&quot;);<br><br>        _orderUpdater.MarkAsPaid(orderId);<br>    }<br>}</pre><p>After this change, PaymentService no longer knows about OrderService directly. It only interacts with the IOrderUpdater abstraction. OrderService provides the implementation, but the dependency is now inverted, which removes the direct cycle between the two components.</p><p><strong>— Restructure Logic</strong></p><p>Circular dependencies are almost always a red flag. It is often a signal that responsibilities are misplaced.</p><p>In that case, the solution is often to reorganize the code in one of the following ways.</p><ul><li><strong>merge responsibilities into a single component</strong></li></ul><p>If two components constantly call each other, they may actually represent one cohesive responsibility that was artificially split. Instead of forcing them to collaborate through a back-and-forth dependency, it can be simpler to combine them into a single component.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/200/1*oRBwMbM4TY8f_mvh580Ntg.png" /></figure><p>This approach works well when the logic naturally belongs to one use case. By merging components, the control flow becomes explicit and easier to reason about.</p><pre>public class CheckoutService<br>{<br>                      .  .  .<br><br>    public async Task Checkout(CreateOrderRequest request)<br>    {<br>        var order = Order.Create(request.Items);<br><br>        var paymentResult = await _paymentGateway.Charge(request.PaymentDetails, order.Total);<br><br>        order.MarkAsPaid();<br><br>        await _orderRepository.Save(order);<br>    }<br>}</pre><ul><li><strong>consolidating responsibility in one of the components</strong></li></ul><p>In case the services are truly independent and cannot be merged, but you still have a cycle, this can mean that their responsibilities were chosen poorly.</p><p>In such cases, the simplest solution is to move the relevant logic into a single component and make it responsible for the entire operation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*s8KD5g3DyIXNcFxt1WzWuw.png" /></figure><p>Here, the responsibility for the process is consolidated in OrderService. It creates the order, triggers the payment, and completes the workflow by marking the order as paid.</p><p>PaymentService only performs its core task—processing the payment—without knowing anything about orders.</p><pre>public class OrderService(PaymentService _paymentService)<br>{<br>    public void CreateOrder(Order order)<br>    {<br>        _paymentService.ProcessPayment(order.Id);<br>     <br>         MarkAsPaid();<br>    }<br>}<br><br>public class PaymentService<br>{<br>    public void ProcessPayment(Guid orderId)<br>    {<br>        Console.WriteLine(&quot;Payment processed&quot;);<br>    }<br>}</pre><p>By clarifying ownership of the workflow, the dependency becomes one-directional, and the cycle disappears.</p><ul><li><strong>introducing a new shared component</strong></li></ul><p>You may find that the functionality doesn’t naturally belong to either service.</p><p>It would be a good idea to extract the shared logic into a separate component. Both services can then use this new component independently, removing the direct dependency between them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*0V0k5VorqwcHpnI0yAu4lw.png" /></figure><p>The shared component handles the cross-cutting functionality (MarkAsPaid), so OrderService and PaymentService no longer depend directly on each other:</p><pre>public class OrderService(<br>  PaymentService _paymentService, <br>  OrderStateManager _orderStateManager)<br>{<br>    public void CreateOrder(Order order)<br>    {<br>        _paymentService.ProcessPayment(order.Id);<br>        _orderStateManager.MarkAsPaid(order.Id);<br>    }<br>}<br><br>public class PaymentService<br>{<br>    public void ProcessPayment(Guid orderId)<br>    {<br>        Console.WriteLine(&quot;Payment processed&quot;);<br>    }<br>}<br><br>// Shared component handling Order state logic<br>public class OrderStateManager(IOrderRepository _orderRepository)<br>{<br>    public void MarkAsPaid(Guid orderId)<br>    {<br>        var order = _orderRepository.Get(orderId);<br>        order.MarkAsPaid();<br>        _orderRepository.Save(order);<br>    }<br>}</pre><ul><li><strong>introduce the coordinator component</strong></li></ul><p>Sometimes a cycle appears because the domain logic is <a href="https://medium.com/@iamprovidence/understanding-object-oriented-design-rich-vs-anemic-models-in-c-5d8e5ea1bf39">anemic</a>, and services at the same layer end up coordinating behavior between themselves.</p><p>Instead of services calling each other, you can introduce an orchestrator above them, usually in an upper layer, such as the <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">Application layer</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*GO3GzXrFprL6p292z46oIQ.png" /></figure><p>This component becomes responsible for sequencing the operations while the lower-level services focus only on their specific tasks.</p><pre>public class OrderApplicationService(<br>    OrderService _orderService,<br>    PaymentService _paymentService)<br>{<br>    public void Handle(CreateOrderRequest request)<br>    {<br>        _orderService.CreateOrder(request.Order);<br>        _paymentService.ProcessPayment(request.Order.Id);<br>        _orderService.MarkAsPaid(request.Order.Id);<br>    }<br>}</pre><ul><li><strong>push the behavior to a deeper layer</strong></li></ul><p>Rather than introducing a component above them, some of the work can be pushed down to deeper layers, such as the <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">domain</a> model or <a href="https://medium.com/@iamprovidence/2210d400db5a/">infrastructure</a> utilities.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*i1QIHnrhIRd60oNY0X9HTw.png" /></figure><p>When the domain object owns the rule, services no longer need to coordinate back and forth, and the cycle disappears.</p><pre>public class OrderService(PaymentService _paymentService)<br>{<br>    public void CreateOrder(Order order)<br>    {<br>        _paymentService.ProcessPayment(order);<br>    }<br>}<br><br>public class PaymentService<br>{<br>    public void ProcessPayment(Order order)<br>    {<br>        Console.WriteLine(&quot;Payment processed&quot;);<br><br>        order.MarkAsPaid();<br>    }<br>}<br><br>class Order<br>{<br>                  .  .  .<br> <br>    public bool IsPaid { get; private set; }<br><br>    public void MarkAsPaid()<br>    {<br>        IsPaid = true;<br>    }<br>}</pre><p><strong>— decouple the system with events</strong></p><p>Another strong solution to <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#f9c9">reduce coupling</a> and break direct dependencies between components is to communicate through <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">events</a>.</p><p>Instead of calling each other directly, services publish events describing what happened, and other components react to those events. This removes the need for services to reference each other.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*MEw0ggEQGMLOZjnP7Qv98w.png" /></figure><p>In this approach, components depend only on an event bus and the event contracts, not on each other’s implementations.</p><pre>public record OrderCreatedEvent(Guid OrderId);<br>public record PaymentProcessedEvent(Guid OrderId);<br><br>public class OrderService(IEventBus _eventBus): IEventHandler&lt;PaymentProcessedEvent&gt;<br>{<br>    public void CreateOrder(Order order)<br>    {<br>        Console.WriteLine(&quot;Create order&quot;);<br><br>        _eventBus.Publish(new OrderCreatedEvent(orderId));<br>    }<br><br>    public void Handle(PaymentProcessedEvent e)<br>    {<br>        Console.WriteLine(&quot;Order marked as paid&quot;);<br>    }<br>}<br><br>public class PaymentService(IEventBus _eventBus) : IEventHandler&lt;OrderCreatedEvent&gt;<br>{<br>    public void Handle(OrderCreatedEvent e)<br>    {<br>        Console.WriteLine(&quot;Payment processed&quot;);<br><br>        _eventBus.Publish(new PaymentProcessedEvent(orderId));<br>    }<br>}</pre><p>Notice that the services no longer depend on each other. However, it is important to remember that logical cycles can still exist in <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">event-driven systems</a>. One event may trigger another, which eventually leads back to the original component, creating an infinite loop of publishes and handlers.</p><h3>Summary</h3><p>In a system with many components, it is inevitable for one to depend on another. Managing those dependencies helps to shape the structure of the system and determine how easy it is to maintain the code.</p><ul><li>Uncontrolled references between components gradually turn a system into a tangled structure where dependencies point in every direction.</li><li>Mixed responsibilities often create cycles, where a change in one component triggers a cascade of changes in another.</li><li>Cycles tightly couple components, making systems harder to understand, test, and change.</li><li>The core goal is to restore one-directional dependencies so the architecture forms an acyclic graph.</li><li>Most solutions fall into three categories: dependency inversion, restructuring responsibilities, or decoupling with events.</li><li>Large cycles can often be fixed simply by resolving a single misplaced dependency.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hxSYkEJeePJH9DgLREnV4Q.png" /></figure><p>Give this article a clap or a few if you like it 👏<br>You can support me with a link below ☕️<br>And don’t forget to follow ✅</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f19688afe006" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Vertical Slice Architecture]]></title>
            <link>https://medium.com/@iamprovidence/vertical-slice-architecture-e3b7b8f48ce9?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/e3b7b8f48ce9</guid>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Sun, 17 May 2026 10:01:06 GMT</pubDate>
            <atom:updated>2026-07-09T09:01:12.659Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1ZwLCembcoyvdB7Yl3pc1g.png" /></figure><p>It is common to build <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4">backend architectures</a> around <strong>horizontal layers</strong>. We separate systems into familiar tiers like <a href="https://medium.com/@iamprovidence/frontend-side-architecture-evolution-mpa-spa-bd87fa32da8">presentation</a>, <a href="https://medium.com/@iamprovidence/2210d400db5a/">infrastructure</a>, <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">application</a>, and <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">domain</a>, each responsible for a specific technical concern.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*-IAPL_Ey06cmDW0DIx_WzQ.png" /></figure><p>However, there is an alternative approach: <strong>vertical architecture</strong>. Instead of organizing code around technical concerns, the system is structured around features, with each vertical slice containing everything it needs — from <a href="https://medium.com/@iamprovidence/services-communication-http-long-polling-server-sent-events-websocket-graphql-soap-grpc-b2e9b331fa54">API</a> to <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">persistence</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/499/1*W8svvnYB8Qf_H7nM9nIsvg.png" /></figure><p>Let’s explore what vertical architecture is, the challenges you may face when adopting it, how to share code effectively, where to draw boundaries, and how features communicate with each other.</p><h3>Problem with Traditional Layered Architecture</h3><p>In a well-known layered architecture, when adding or modifying a feature, the change rarely stays in one place. Even a small functionality typically forces you to touch multiple layers at once:</p><ul><li>you start by updating the <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">Domain</a>, changing <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">entities</a>, introducing new rules and constraints</li><li>then you add new tables, <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">repositories</a>, or DB objects, corresponding mappers in the <a href="https://medium.com/@iamprovidence/2210d400db5a/">Infrastructure layer</a></li><li>after that, you modify or create a use case, adjust validation logic, add DTOs, and more mapping code in the <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">Application layer</a></li><li>finally, you expose everything through a controller or API endpoint in the <a href="https://medium.com/@iamprovidence/frontend-side-architecture-evolution-mpa-spa-bd87fa32da8">Presentation layer</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*jVhPsneY2Q7YqlKYKxXr4A.png" /></figure><p>Instead of focusing on the feature itself, you end up coordinating changes across technical boundaries. This increases cognitive load and raises the risk of breaking unrelated parts of the system.</p><p><em>Layers are about managing </em><a href="https://medium.com/@iamprovidence/77d06d44af0d#36ca"><em>coupling</em></a><em> in a clear way, not </em><a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#ce24"><em>cohesion</em></a><em>.</em></p><h3>What Vertical Slice Architecture Is</h3><p>Vertical slice architecture takes a different approach.</p><p><em>Slices are about maximizing </em><a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#ce24"><em>cohesion</em></a><em> within a slice and minimizing </em><a href="https://medium.com/@iamprovidence/77d06d44af0d#36ca"><em>coupling</em></a><em> between them.</em></p><p>It builds on ideas from <a href="https://medium.com/@iamprovidence/d96bdd6a526f/">Screaming Architecture</a>, but puts it further. Slices organized the code <strong>by feature</strong>, not by technical concern. This means that each <strong><em>slice contains</em></strong> everything it needs (API, use case, validation, business logic, persistence, infrastructure, etc).</p><p>So if you need to update functionality, all the changes will be concentrated within a single slice.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*or8EPE4bfVPYnxScZNZBtg.png" /></figure><p>The benefits are:</p><ul><li>changes are localized within a single slice</li><li>code navigation becomes simpler</li><li>folders remain small and focused, making them easier to manage and maintain</li><li>the risk of breaking unrelated functionality is reduced</li><li>features evolve independently</li><li>teams work in parallel</li><li>developers better understand the system&#39;s business capability</li><li>it’s easier to understand a module, its available use cases, the used infrastructure, etc</li><li>adding a new feature is easier, as you can identify similar behavior and replicate the structure</li><li>removing a feature is straightforward because you can simply delete the entire folder without affecting unrelated code</li><li>refactoring becomes easier; it is simpler to move, reorganize, or restructure functionality without impacting other parts of the system</li><li>extracting functionality from a folder into a separate project becomes easier, since related code is already grouped and self-contained</li><li>and so on and so forth…</li></ul><p>You may be thinking: “Sounds like some kind of magic. So why isn’t everyone applying it everywhere? 🤔”.</p><p>Don’t push the horses. There are reasons 😅. The most common problems are:</p><ul><li>slice granularity</li><li>code sharing</li><li>slices communication</li></ul><p>Let’s address each problem one by one.</p><h3>Slice Granularity</h3><p>You might think, in theory, that you know where one slice ends and another begins. Buuuut… in practice, you’ll encounter many pitfalls before getting it right 😒.</p><p>While, for most people, grouping code by categories such as business logic or API endpoints is relatively simple to comprehend, organizing code within slices is more complex.</p><p>Defining boundaries is hard. Should you group around <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">an entity</a> or <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#2218">an aggregate</a>? Around an action or a <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">use case</a>? Around a feature?</p><p>There is no one good solution.</p><p>Some claim that slicing by actions is wise, as you can use different tools for each action, but this is rarely practical and results in too thin slices.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/716/1*p8Oh6Z1xIGYB5vAYmXp3lg.png" /></figure><p>You can also group by entities, as it feels more intuitive. This way, you can combine all operations over a single entity into one slice. But business concepts often span multiple entities. Therefore, it’s usually better to organize code around aggregate boundaries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/350/1*VrkBc5gprDIFUFBmEyLWlg.png" /></figure><p>Finally, and the way I recommend, is to design each slice around the entire feature. It is the broadest but also the most isolated concept.</p><p>But what is even a feature 🤔?</p><blockquote>A <strong>feature</strong> is a complete business capability that enables a user to achieve a specific goal. It represents a meaningful outcome, delivers measurable value, and can be understood, discussed, prioritized and used independently.</blockquote><p>Such features could be course offering, student management, enrollment workflow, etc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/773/1*nyILoLzG2QHZ1u8oBfH2AQ.png" /></figure><p>A feature may span multiple aggregates, or it may be contained within just one endpoint, depending on the scope of the business capability it represents. You should define a slice around functionality that is meaningful to your business stakeholders, and not by the number of entities it includes. The key is that each slice fully encapsulates the feature it serves, keeping it cohesive, understandable, and easy to evolve.</p><p>Long story short, if your slices are too thin, you’ll likely end up with more shared code and duplication. On the other hand, slices that are too chunky risk turning into spaghetti code. This architecture demands stronger discipline, thorough <a href="https://medium.com/@iamprovidence/6d75c8886763/">code reviews</a>, and constant boundary re-evaluation.</p><h3>Code Sharing</h3><p>In vertical slice architecture, your goal is to have independent, self-contained slices. Buuuut… it is easier said than done 😒.</p><p>Sometimes you need to share <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#d6e1">business invariants</a>, rules, or even entities across multiple slices; other times, what needs to be shared is infrastructure, such as <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84">caching</a>, <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">persistence</a>, or <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logging</a>.</p><p>Therefore, you typically introduce layers above and/or below the slices. A common approach is to add the following supporting layers:</p><ul><li><strong>Core</strong> — define a <em>foundation for your slices</em>, containing part of the framework (composition root, UI pages layout, middlewares, etc)</li><li><strong>Shared</strong> — shared infrastructure code, such as persistence, caching, logging, message bus, <em>reused across multiple</em> <em>slices</em></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/536/1*UGqCJuNWhiIbsA7kRHgnUA.png" /></figure><p>Notice that even here we have <strong>dependency rules</strong>: references should flow from top to bottom, not the other way around, and <em>never</em> horizontally between slices.</p><p>For example, your Core module references each slice to execute an action triggered by the UI:</p><pre>[ApiController]<br>[Route(&quot;api/course&quot;)]<br>public class CourseController : ControllerBase<br>{<br>    [HttpPost(&quot;create&quot;)]<br>    public IActionResult CreateCourse([FromServices] ICourseSliceApi sliceApi)<br>    {<br>        var result = sliceApi.CreateCourse();<br><br>        return Ok(result);<br>    }<br>}</pre><p>On the other side, your slice can define the interface:</p><pre>public interface ICourseDbContext<br>{<br>    DbSet&lt;Course&gt; Courses { get; }<br>    DbSet&lt;Lesson&gt; Modules { get; }<br>    DbSet&lt;Lesson&gt; Lessons { get; }<br><br>    Task&lt;int&gt; SaveChangesAsync(CancellationToken cancellationToken = default);<br>}</pre><p>Where the implementation will be provided in the underneath shared layer:</p><pre>public class AppDbContext : DbContext, <br>  ICourseDbContext,  // 👈 course slice <br>  IStudentDbContext, // 👈 students slice<br>  ...<br>{<br>    public DbSet&lt;Course&gt; Courses { get; set; }<br>    public DbSet&lt;Module&gt; Modules { get; set; }<br>    public DbSet&lt;Lesson&gt; Lessons { get; set; }<br><br>    public DbSet&lt;Student&gt; Students { get; set; }<br><br>    public DbSet&lt;Enrollment&gt; Enrollments { get; set; }<br>    <br>    protected override void OnModelCreating(ModelBuilder modelBuilder)<br>    {<br>        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext));<br>    }<br>}</pre><p>This approach allows each interface to expose only the functionality a slice actually needs, which significantly reduces cognitive load.</p><p>The trade-off is that the shared layer ends up depending on all slices. This can become problematic: a change in one slice might propagate to the shared layer, potentially impacting other slices indirectly. To mitigate this, you can split the shared layer to limit the ripple effect and to avoid referencing <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#2cd4">unnecessary classes</a>.</p><p>In practice, many teams keep all slices in a single project and separate them by folders; this keeps things simple and flexible. Moving each slice into its own project gives you stronger, compiler-enforced boundaries, but it also adds overhead and can be harder to maintain as the number of slices grows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/535/1*kgNt0cqvcrgFJ0xP2L4xkg.png" /></figure><p>However, some people go even further, and keep all three layers (Core, Slices, Shared) in a single project😅.</p><p>This can work for small projects, but within a team quickly results in a mess. Nothing prevents breaking a dependency rule. There are no clear <a href="https://medium.com/@iamprovidence/b1fafacf7a90/">boundaries</a> — no compilation errors or project reference restrictions — to enforce separation.</p><p>As a result, people coming from other architectures, like <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#8386">Hexagon</a>, where the Core layer has a different meaning, tend to put <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#d6e1">domain rules</a>, <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#a1f5">value objects</a>, <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#98af">services</a>, <a href="https://medium.com/@iamprovidence/context-a-vital-pattern-nobody-talks-about-6a09711a98d8">current user context</a>, and basically anything that represents core concepts of the system there, making the code harder to maintain for everyone😒.</p><p>Your team should switch mindset and understand that this isn’t layered architecture: a slice’s <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">business logic</a> and <a href="https://medium.com/@iamprovidence/2210d400db5a/">infrastructure</a> should remain within that slice, and shared functionality should go into the shared layer. However, it usually takes a long review cycle — some gentle scolding included — before the team fully gets it 😁.</p><h3>Slices Communication</h3><p>If you properly pick boundaries between slices, they would barely need to communicate. Buuuut… sometimes they do 😒.</p><p>If you’re familiar with similar architectures — where systems are organized around <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#2785">bounded contexts</a>, like in a <a href="https://medium.com/@iamprovidence/modular-monolith-6bb4be9232cd">Modular Monolith</a> or <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">Microservices</a> — you might feel confident and be tempted to implement a solution the same way, with slices interacting exclusively through a public API 🤓.</p><p>But those are slices, not modules. We don&#39;t need to overcomplicate stuff. To fetch data from another slice, you can rely on shared infrastructure and use the database tables you need.</p><pre>public interface IStudentDbContext<br>{<br>    // 👇 Access data owned by another slice in a read-only manner<br>    IQueryable&lt;Course&gt; Courses { get; } <br>    <br>    DbSet&lt;Lesson&gt; Students { get; }<br><br>    Task&lt;int&gt; SaveChangesAsync(CancellationToken cancellationToken = default);<br>}</pre><p>If you not only need to fetch data, but also to perform an operation in another slice, instead of triggering an action, it’s better to take the reverse approach: publish an event via a <a href="https://medium.com/@iamprovidence/component-communication-in-angular-for-senior-devs-4e3c9f0f2c59#2e4e">mediator</a> or an <a href="https://medium.com/@iamprovidence/component-communication-in-angular-for-senior-devs-4e3c9f0f2c59#b767">in-memory message bus</a> (see <a href="https://gist.github.com/iamprovidence/aa3f72cf647055003e8d41cc2fb1e54d">github</a> example), allowing the other slice to react to it.</p><p>For example, one slice publishes an event:</p><pre>// Event definition<br>public record CoursePublishedEvent(Guid CourseId) : IEvent;<br><br>// Course slice publishes the event<br>public class CourseService(IEventBus _eventBus)<br>{<br>    public void PublishCourse(Guid courseId)<br>    {<br>                  .  .  .<br>        <br>        // Notify other slices<br>        _eventBus.Publish(new CoursePublishedEvent(courseId));<br>    }<br>}</pre><p>And another slice performs the callbacks.</p><pre>// Students slice reacts to the event<br>public class NotifyStudentsHandler : IEventHandler&lt;CoursePublishedEvent&gt;<br>{<br>    public void Handle(CoursePublishedEvent @event)<br>    {<br>        var students = _dbContext.Students.ToList();<br><br>        foreach (var student in students) <br>        {<br>            _emailService.SendEmail(student, $&quot;New Course is available: {@event.Course.Name}&quot;)<br>        }<br>    }<br>}</pre><p>With the <a href="https://medium.com/@iamprovidence/988008ccf2be/">event-driven paradigm</a>, you don’t have direct method calls, and this keeps slices <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#f9c9">decoupled</a> while still enabling cross-feature reactions.</p><h3>Summary</h3><p>This architectural style goes by several names: Vertical Architecture, Slice Architecture, or Feature-Based Architecture.</p><p>Vertical Slice Architecture isn’t just about having neatly organized folders; it also defines the purpose of the Core and Shared layers and clarifies the dependencies between them, slices granularity, and how they communicate.</p><p>It’s a luck to see Vertical Slice applied in backend systems, though some projects — like <a href="https://github.com/DuendeArchive/IdentityServer.Templates/tree/main/src/IdentityServerAspNetIdentity/Pages">IdentityService</a> — adopt it successfully. It is more commonly used in frontend frameworks such as <a href="https://github.com/iamprovidence/Lama/tree/development/src/frontend/Angular/src/app">Angular</a> or React, where concerns like tight coupling to the UI framework are less of an issue. In these environments, features don’t have complex domain logic and don&#39;t depend on external infrastructure dependencies, making slices easier to implement.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/843/1*zvk8ffaxghubtBUvAqrJVQ.png" /></figure><p>While Slices offer clear benefits, they come with <em>trade-offs</em> that are important to understand:</p><ul><li>it is designed to optimize for <em>change</em> rather than <em>reuse</em></li><li>it <em>emphasizes cohesion</em> within a feature rather than minimizing coupling between components</li><li>this approach makes it harder to define feature boundaries</li><li>intentional code duplication is common, and reusing “shared” becomes more difficult</li><li>if a lot of code must be shared between slices, that’s usually a sign that the boundaries have been drawn incorrectly</li><li>core and shared layers have references across slices, making them harder to replace, as they are <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#6b9e">stable components</a></li><li>without high discipline in a team and <a href="https://medium.com/@iamprovidence/b1fafacf7a90/">physical boundaries</a>, a project can turn into a “big ball of mud”, where responsibilities overlap, logic is duplicated, and features unnecessarily reference one another</li></ul><p>That said, let’s hope I haven’t scared you too much, and have encouraged you just enough to give it a try 🙃. For those who get that far, as always a small joke:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xc2w_fx-jwtFCqQQosS4Tg.png" /></figure><p>Don’t forget to give the article a rating if you liked it 👏<br>Let me know if you have faced this architecture in practice💬<br>You can support me using the link below ☕️<br>Share with friends 🗣 Slice, by slice 🤫<br>Check out my other articles ⬇️<br>And don’t forget to follow ✅<br>See Ya😉</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e3b7b8f48ce9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ultimate guide to PR review process]]></title>
            <link>https://medium.com/@iamprovidence/ultimate-guide-to-pr-review-process-6d75c8886763?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/6d75c8886763</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[git]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Thu, 07 May 2026 10:01:01 GMT</pubDate>
            <atom:updated>2026-05-10T14:34:48.344Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/980/1*nArWSC7CAT5VfGlNoQC2Eg.png" /></figure><p>Yeap, another article about PR reviews 😅. This one’s going to be a big one — however, it’s not for you guys, it’s for me. It’s a personal reminder on how to conduct reviews and document new projects. If you find it useful, great 😁! If not, that’s unfortunate 😔.</p><p>There are some general topics I would like to discuss:</p><ul><li>What Is a Code Review?</li><li>Motivation</li><li>Knowledge Sharing</li><li>Who Should Review Your Code?</li><li>How Many Reviewers Should Be Assigned?</li></ul><p>And give advice to each party:</p><ul><li>Rules for the Team</li><li>Rules for Code Review Creators</li><li>Rules for Reviewers</li></ul><p>Still, I hope you will like it. Let’s see 🙃.</p><h3>What Is a Code Review?</h3><p>A code review is the process of examining someone’s code before it’s merged into the main branch.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/790/1*gbUTevWzaahrt9lfJC-mhw.png" /></figure><h3>Motivation</h3><p>Code reviews motivate developers to write high-quality code, knowing it will be examined by their peers:</p><ul><li><strong>Code reuse:</strong> Encourages leveraging existing functionality instead of reinventing the wheel.</li><li><strong>Code style consistency:</strong> Reviewers ensure new code follows established style guides before it reaches the main branch.</li><li><strong>Error detection:</strong> Reviewers help catch typos, legacy fragments, redundant comments, or stray console <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logs</a>.</li><li><strong>Bug reduction:</strong> Early feedback lowers the number of issues reaching production.</li><li><strong>Less refactoring:</strong> Developers can address concerns before merging, reducing the need for later fixes.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CAKDqIbbdzxFFfjULXYNAQ.png" /></figure><blockquote><strong><em>Notice ⚠️:</em></strong></blockquote><blockquote>Many teams choose to skip code reviews, believing they’re time-consuming and rarely catch significant bugs. They usually limit reviews to newcomers or less experienced developers. That approach can work. I’ve tried it myself, and it was actually refreshing, especially in a toxic environment where reviews focused more on blank lines than real issues. However, in a healthy, well-structured team, code reviews often prove their worth.</blockquote><h3><strong>Knowledge Sharing</strong></h3><p>Code reviews foster continuous learning and team growth.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/183/1*ip3ZQ-W2dmP-vqsNGeMFUg.png" /></figure><p>They create an environment where knowledge flows freely, encouraging everyone to become both a teacher and a learner:</p><ul><li><strong>Learn from Others:</strong> Reviewers discover new algorithms, patterns, and techniques by exploring different coding styles.</li><li><strong>Reasoning Skills:</strong> Explaining and defending decisions helps developers think critically about their code and understand why one solution is better than another.</li><li><strong>Gain Feedback:</strong> Authors receive valuable input and suggestions that can improve their skills and solution design.</li><li><strong>Fresh Perspective:</strong> A new set of eyes can spot alternative approaches or potential improvements.</li><li><strong>System Awareness:</strong> All participants become more familiar with the system’s new or modified components.</li></ul><h3>Who Should Review Your Code?</h3><p>Ideally, every developer should regularly participate in the review process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*X-jpA-8cQADx8VeDffOGhA.png" /></figure><p>However, some teammates may have deeper expertise. It is recommended to assign:</p><ul><li>A senior developer or architect capable of spotting structural and architectural issues.</li><li>Someone familiar with the functionality you’re modifying.</li><li>A responsible teammate you trust 🙃.</li></ul><p>Do not overload the same reviewers with every pull request. Let’s let those people rest, for God’s sake 😭. Code reviews work best when everyone shares the load (and the blame) evenly across the team.</p><h3>How Many Reviewers Should Be Assigned?</h3><p>The optimal number of reviewers depends on the complexity and impact of the change.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*Kp-7g4Si1Z7Sr7XSvrPC3g.png" /></figure><p>For small fixes or minor updates, <strong>one reviewer</strong> is usually enough to ensure code quality and consistency.</p><p>For larger features, architectural updates, or critical modules, it’s best to have <strong>two reviewers</strong>, based on <a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/git-branching-guidance?view=azure-devops#review-and-merge-code-with-pull-requests">research</a>.</p><p>Avoid overloading the process with too many reviewers, as it can slow down development and dilute responsibility. The goal is to maintain quality while keeping the feedback loop fast and focused.</p><h3>Rules for the Team</h3><p>As a team, you should establish your PR review process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*fUf2mr393uXqnM_jNENWpw.png" /></figure><p>Ensure your PR review process is clearly documented and agreed upon by everybody. Meet with your team to define:</p><h4><strong>📏 PR Size &amp; Structure</strong></h4><ul><li><strong>Maximum PR Size:</strong> Define the largest pull request each team member is comfortable reviewing. Smaller, frequent PRs are easier to review thoroughly.</li><li><strong>Handling Large or Complex Features:</strong> Agree on splitting large features into multiple PRs and using intermediate reviews.</li></ul><h4>⏰ Review Timing</h4><ul><li><strong>Review Turnaround Time:</strong> Set expectations for how quickly reviews should be completed.</li><li><strong>Maximum Time Before Review:</strong> Agree on how long a PR can remain unreviewed.</li></ul><p>I don’t even remember what I had for lunch today; if you ask me a week later why I wrote the code this way, I’d be explaining it to myself 😒!</p><h4>👥 Roles &amp; Assignments</h4><ul><li><strong>Reviewer Assignment:</strong> Decide whether PRs are assigned to specific reviewers or rotated across the team.</li></ul><h4>🧾 Process &amp; Standards</h4><ul><li><strong>PR Template:</strong> Agree on mandatory checklist items for all PRs to ensure consistency in quality and style. Configure <a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository">PR template</a> for your repository:</li></ul><pre>✅ My code follows the style guidelines of this project<br>✅ I have performed a self-review of my own code<br>[] I have commented my code, particularly in hard-to-understand areas<br>[] My changes generate no new warnings<br>[] I have added tests that prove my fix is effective or that my feature works<br><br>                               . . .</pre><ul><li><strong>Code Review Pyramid:</strong> Agree on a <a href="https://www.morling.dev/blog/the-code-review-pyramid/">code review pyramid</a> to focus on those parts that matter the most during a code review.</li></ul><h4>💬 Communication &amp; Feedback</h4><ul><li><strong>Commenting Guidelines:</strong> Standardize how to provide constructive feedback, including templates, tone, and when to escalate issues.</li><li><strong>Conflict Resolution:</strong> Establish a clear process for disagreements between reviewers and authors.</li></ul><h4>⚙️ Automation &amp; Quality Gates</h4><ul><li><strong>Automated Checks:</strong> Determine which tasks (linters, formatters, tests, security checks) should be automated before human review.</li><li><strong>Approval Criteria:</strong> Define what conditions must be met before a PR can be approved and merged.</li></ul><h3>Rules for Code Review Creators</h3><p>A well-prepared author makes the review process smoother and more productive for everyone involved.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*awnodaBDQfHRJ7Shn8rfpg.png" /></figure><p>The author of a code review should follow these key rules to keep the process efficient and professional.</p><h4>💡 Preparation</h4><ul><li><strong>Prepare Before Submitting. </strong>Make sure your code compiles, there are no merge conflicts, it passes all tests, and follows the team’s style guide. Don’t submit half-done work.</li><li><strong>Self-Review.</strong> Review your code first — check clarity, complexity, formatting, and tests. Fix what you can before submission. Seeing the full picture often helps you catch things you initially missed.</li></ul><h4>🤝 Collaboration</h4><ul><li><strong>Resolving Comments. </strong>All reviewer comments must be addressed. It’s not acceptable to close the comments without discussion.</li><li><strong>Communicate Clearly. </strong>If you disagree with feedback, discuss it respectfully and provide reasoning. The goal is collaboration, not debate.</li></ul><h4>🧠 Mindset</h4><ul><li><strong>You Are Not Your Code. </strong>The purpose of a review is to improve the code, not to judge the developer. Expect feedback and remember — comments target the code, not you.</li><li><strong>Learn from Feedback. </strong>Each review is an opportunity to improve. Pay attention to recurring comments — they often reveal areas where you can grow as a developer.</li></ul><h3>Rules for Reviewers</h3><p>I found it more entertaining to review someone’s code than being reviewed. Creating a PR is easy; reviewing one is where the real challenge lies — and the real skill shows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*rzZJ2JyaJr6mg_hx-12-EQ.png" /></figure><p>Where else would you be able to compare others to garbage to feel smarter yourself 😅. Therefore, let’s see a list of practices on how to do it professionally.</p><h4>⚡ The Right Tone in Reviews</h4><p>This may surprise you — especially if you’re the people-type — but sometimes a little tension is healthy.</p><p>A good code review isn’t a tea party; it’s a constructive clash of ideas. When people like you too much, they may avoid challenging your decisions to keep the peace. But if they respect you — even if they occasionally dislike you — they’ll speak up. That’s where real technical growth happens.</p><p>The secret here is to be toxic just enough not to get fired.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/1*GGWzGQ7g-Intyb-n47RjMw.jpeg" /></figure><p>Diamonds form under pressure; so does great code.</p><h4>🤖 Automate Code Review</h4><p>Before even starting a review, you should make sure it passes all automated checks.</p><p>I’ve worked with developers who’d spend hours pointing out in review formatting and syntax rules. What a waste of time and the customer’s money 😒. We’re developers, after all 😤. Spend five days to automate your five-minute routine 😁!</p><p>Here are some tasks worth automating:</p><ul><li>This one is kind of obvious, but still, <strong>builds and </strong><a href="https://medium.com/@iamprovidence/unit-tests-018611590eac"><strong>unit tests</strong></a> should always be automated to catch errors early.</li><li><strong>Branch, </strong><a href="https://github.com/marketplace/commitcheck"><strong>commit</strong></a><strong>, PR name validation</strong> to keep naming consistent</li><li><strong>Linters</strong> to enforce coding style rules.</li><li><strong>Code formatters (beautifiers)</strong> to standardize formatting automatically.</li><li><a href="https://medium.com/@iamprovidence/architecture-tests-878d76e4fd56"><strong>Architecture and convention checks</strong></a> to ensure the code follows project structure rules, layer boundaries, and established naming or conventions.</li><li>Use<strong> automated code review tools</strong> that generate comments using code analysis or AI, like <a href="https://www.codacy.com/quality">Codacy</a>, <a href="https://www.sonarsource.com/products/sonarqube">SonarQube</a>, etc</li><li>Find<strong> </strong><a href="https://github.com/marketplace?type=apps&amp;category=code-review"><strong>extensions</strong></a> that will help your project — for example, tools that automatically create work items when a <a href="https://github.com/apps/todo">TODO</a> is detected in the code, etc.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*4wcaQ74asgRB4wWefWrgsA.png" /></figure><p>Automated checks should be run at different stages to catch issues early and ensure code quality.</p><ul><li><strong>On-save actions:</strong> Automatically run linting, formatting, or basic static checks when the file is saved in the IDE.</li><li><a href="https://githooks.com"><strong>Pre-commit or pre-push Git hooks</strong></a><strong>:</strong> Perfect for repository-level rules (branch or commit name).</li><li><a href="https://medium.com/@iamprovidence/devops-for-devs-4bd70721f6bf#b382"><strong>CI pipeline</strong></a><strong>:</strong> Triggered when a pull request is created. Ideal for running builds, executing <a href="https://medium.com/@iamprovidence/unit-tests-018611590eac">tests</a>, and performing deeper analysis.</li></ul><p>This lets the reviewer focus on what truly matters — logic, design, and architecture — instead of formatting or syntax.</p><h4>🎯Understand the Purpose Before Reviewing</h4><p>Code becomes far easier to understand when you know <strong>why</strong> it was written.</p><p>Begin by reading the ticket thoroughly and identifying the code that addresses the stated requirements.</p><p>Then, run the code to verify it actually works. You don’t need to act as <a href="https://medium.com/@iamprovidence/manual-testing-0f6367325f56">QA</a> or test every edge case — just make sure the main “happy path” functions as expected. Otherwise, you may approve good-looking code that doesn’t even work. What was even the point of doing the review?</p><h4>🧐 What Should Be Reviewed</h4><p>Focus on what truly matters — <strong>logic, </strong><a href="https://medium.com/@iamprovidence/remove-your-code-f9f2acdc50dd"><strong>redundant code to remove</strong></a><strong>, design, convention, and </strong><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4"><strong>architecture</strong></a>. The goal of a review isn’t to argue over spaces, bracket placement, or be nitpicky, but to ensure the code makes sense.</p><p>A good reviewer looks for clear logic, maintainable structure, and alignment with architectural standards.</p><h4>✅ Have a Code Review Checklist</h4><p>It’s easy to overlook important details during a code review, which is why having a checklist is crucial. A checklist ensures consistency, helps reviewers cover all key points, and makes the review process more thorough and effective.</p><p>It defines what must be verified in every pull request:</p><pre>Git:<br>✅ Branch name is correct<br>️✅️️ Commits names are correct<br>️✅ PR name is correct<br>️✅ Work item is linked<br>✅️ Is the pull request merged into the correct branch<br><br>Code:<br>✅️ Folders / Files structure is reasonable<br>❌ Code is covered with tests<br>✅ Functions and variables are well-named <br>️✅ No duplicated code <br>️✅ Proper error handling <br>[] Code is properly formatted<br>[] Input validation implemented <br>[] Performance considerations addressed<br><br>Performances:<br>[] Are introduced changes scalable?<br>[] Will it cause performances issues?<br><br>Maintenance:<br>[] Do we have enough logging do debug on production?<br>[] Do logs contain enough details (entity ID, email, names, trace ID)?<br><br>DB:<br>[] Is the database structure (if any changes) correct?<br>[] Does it contain the correct constraints?<br>[] Is the naming for new tables/columns correct?<br><br>Security:<br>️✅️️️ Permissions are managed properly<br>[] No exposed secrets or API keys<br><br>Docs:<br>️✅ Documentation is created/updated if needed</pre><p>The exact content depends on your team’s priorities, but it should cover everything you consider critical for stability, security, and readability.</p><p>And if you think some of the items can be automated, do it 🙃.</p><h4>💻 Review Code in an IDE</h4><p>Sure, reviewing code in a browser is faster, but try checking out the branch in your IDE instead.</p><p>Comparing branches lets you explore the file structure, navigate to references, and see where functions are declared.</p><p>Trust me — reviewing in an IDE is far more efficient than in a browser.</p><h4>💬 When to Leave a Comment — and When Not</h4><p>Learn to work in a team and accept non-ideal solutions. Only comment when one of the items in your checklist fails, the code affects functionality, readability, maintainability, <a href="https://medium.com/@iamprovidence/you-should-not-care-about-performance-and-here-is-why-42b327597f6d">performance</a>, or <a href="https://medium.com/@iamprovidence/avoid-those-vulnerabilities-ddos-sql-injection-open-redirect-xss-csrf-clickjacking-0d5de7c74569">security</a>, or you found a bug.</p><p>Look for potential enhancements: new abstractions, reusable code, libraries, or better organization. Watch for overly complex logic, long methods, or code that relies on too many comments to explain itself.<br>Find a method full of nested conditionals, and suggest breaking it into smaller, well-named helper methods to make it cleaner and easier to follow.</p><p>Don’t demand a complete rewrite. First, understand why the current approach was chosen. If you genuinely have a better solution, discuss it with the team — but keep time management and priorities in mind.</p><p>Avoid commenting on minor stylistic preferences — automation already handles that.</p><h4>🔥How to tell people their code is bad</h4><p>Other people’s code will often seem worse — simply because it wasn’t written by you.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/805/1*tat2YvIgIn0TrREnkRvFXQ.png" /></figure><p>However, you can’t just tell someone their code is “bad”. For some reason, people tend to get upset (and this is a pure mystery to me) when you call their work pathetic 😒. You need to have good reasons for that. The key is to learn how to give feedback constructively.</p><p>Avoid vague or condescending comments. These phrases do more harm than good:</p><ul><li>“Can you make it clearer?” assumes the code is unclear without explaining what is confusing.</li><li>“Why didn’t you write it this way?” implies the developer was wrong, careless, or lacked skills or knowledge.</li><li>“Do it differently/clearer/etc” gives no direction and leads to endless back-and-forth.</li></ul><p>Be specific. Explain what’s wrong, why it’s a problem, and how it can be improved. Do not assume your solution is automatically better. Prove it.</p><p>Use a clear <em>comment template</em>:</p><pre>Priority:<br>- Suggestions (consider improving)<br>- Warnings (should fix)<br>- Critical issues (must fix)<br><br>What is wrong: ***<br>Why it&#39;s wrong: ***<br>Suggested improvement: ***</pre><p>This format helps the developer understand exactly what needs to be improved and why, and it helps the reviewer to improve reasoning skills.</p><p>Code can always be written differently — there’s more than one way to solve the same problem. Learn to clearly explain and justify your reasoning. If your proposed change adds little real value, it’s probably not worth leaving a comment. If you feel that something is wrong, but <em>do not have any suggestions</em>, maybe it is not worth writing a comment at all.</p><p>Of course, you can skip the template for obvious issues like types, formatting, or minor typos.</p><p>Make your comments easy to read to minimize misunderstandings.</p><h4>❓ How to ask questions</h4><p>If something in the code isn’t clear, ask. Questions reveal hidden issues, clarify intent, and often spark discussions that lead to better design and shared understanding.</p><p>However, asking questions feels bad sometimes because it can be interpreted as criticism — especially if phrased bluntly (“Why did you do it this way?” sounds like “You did it wrong”). Developers often tie their identity to their code, so even neutral questions can feel personal.</p><p>The key is how you ask. Prefer curiosity over accusation — e.g., “Can you help me understand why this approach was chosen?” assume positive intent — the author likely had a reason.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/300/1*rKV__vv0Q1xWmalIFleiTw.jpeg" /></figure><p>You might uncover the reasoning behind the decision — maybe it was based on outdated information, time pressure, limited experience, or constraints you weren’t aware of. Avoid assuming your approach is better unless you have solid evidence to support it.</p><h4>🤝 Discuss Code Together with the Author</h4><p>Even with a clear comment template, there is always a risk of miscommunication.</p><p>Written comments take time, and explanations lead to long threads. Just call the person, review together, and discuss. The author can clarify why a particular solution was chosen, avoiding weeks of back-and-forth.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ba8UAcHj7CCJPUj73MtOGg.png" /></figure><h4>💎 Learn from Bad Reviews</h4><p>It’s often easier to learn from bad reviews than from good ones. Notice what irritates you when someone reviews your code — vague comments, harsh tone, or nitpicking. Remember how that feels, and make sure you don’t repeat the same mistakes when reviewing others’ work. Bad experiences can teach you how to give better, more respectful feedback.</p><h3>Summary</h3><p>Code reviews in pull requests aren’t just about catching obvious bugs — that’s what <a href="https://medium.com/@iamprovidence/code-testing-cdcff3c48f07">tests</a> are for. A solid review uncovers design issues that could become serious problems later. Additionally, they strengthen collaboration and communication among developers while ensuring consistent quality across the project.</p><p>If you want to conduct a professional code review, switch to the branch and open the PR in your IDE, use a review template, avoid nitpicking, focus your comments on meaningful issues, have a good argument for each point, and discuss each comment to prevent misunderstandings.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n39RyX1Pepk5gbFUD9go9g.png" /></figure><p>💬 Let me know what pisses you off during PR</p><p>👏 Clap for this article if you enjoyed</p><p>☕️ You can also buy me a coffee with the link below</p><p>✅ Follow to not miss out on more</p><p>😉 But most importantly, keep going</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6d75c8886763" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Designing Boundaries: The Art of Splitting Systems]]></title>
            <link>https://medium.com/@iamprovidence/designing-boundaries-the-art-of-splitting-systems-b1fafacf7a90?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/b1fafacf7a90</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Mon, 27 Apr 2026 10:01:02 GMT</pubDate>
            <atom:updated>2026-05-20T09:06:44.607Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2ITHoMhB4twi1h_3fG7xeQ.jpeg" /></figure><p>Have you ever wondered what the difference is between a folder and a project? While it may seem minor at first, a simple decision can save you from future headaches, make dependencies clearer, and make your system much easier to maintain.</p><p>In today’s article, we’ll talk about what a component really is, how to separate components properly, and why those boundaries matter. We’ll look at the difference between organizing code with folders, enforcing structure with projects, and isolating systems at the application level.</p><p>Without any further ado, let’s begin.</p><h4>What Is a Component?</h4><p>Regardless of the <a href="https://medium.com/@iamprovidence/4ddaf42d1a18/">scale</a> of your architecture, it is always the same: you have components, and a relation between them.</p><p>A component can depend on, collaborate with, contain, expose, or hide other components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*851FSXZIyJFkYsvN6Pypyg.png" /></figure><p>But what is even a component?</p><p>It can be a class, a folder, a project, a <a href="https://medium.com/@iamprovidence/modular-monolith-6bb4be9232cd">module</a>, or even a <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">microservice</a>. It is something that owns its data, exposes behavior, <a href="https://medium.com/@iamprovidence/object-oriented-programming-078c2cb21b22#6e91">hides implementation details</a>, and can evolve independently (ideally).</p><p>Therefore, let’s have a clear definition:</p><blockquote><em>A component is a cohesive part of a system that does one thing well and interacts with the rest of the system through explicit contracts.</em></blockquote><p>However, a folder is not automatically a component. A project is not automatically a component. A boundary is what makes it one.</p><h4>Where Should You Draw the Boundary?</h4><p>The moment you draw a boundary between components, you create an internal structure, a public interface to interact (API), and a <a href="https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e">direction of dependencies</a>. And that’s where <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4">architecture</a> begins.</p><p>There are <em>different reasons</em> why you can create a boundary.</p><p>You can separate by <em>technical concerns</em> — <a href="https://medium.com/@iamprovidence/55f35ab38f45">business logic</a>, <a href="https://medium.com/@iamprovidence/2210d400db5a/">infrastructure</a>, <a href="https://medium.com/@iamprovidence/frontend-side-architecture-evolution-mpa-spa-bd87fa32da8">UI</a>. That’s the classic <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#67ea">layered approach</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5DsdlmOm1V3UnVZG2ni7XA.png" /></figure><p>You can draw boundaries around <em>business capabilities</em> (Orders, Payments, etc), as encouraged in <a href="https://medium.com/@iamprovidence/55f35ab38f45">Domain-Driven Design</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/773/1*nyILoLzG2QHZ1u8oBfH2AQ.png" /></figure><p>You can also separate by <em>rate of change</em>, following the <a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#2d8c">Single Responsibility Principle</a>. If the parts change for different reasons, they deserve a boundary.</p><p>There’s a well-known <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#2cd4">trilemma</a> in architecture: <em>releases</em>, <em>responsibility</em>, or <em>code reuse</em>. As with other trilemmas, you have to choose two principles and sacrifice the third one 😒.</p><p>Place your fingers on two nodes, and the line between them shows what you had to deal with.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*BcXC1yls5SpgzOJR.png" /></figure><p>There are other valid reasons to create a boundary. You might split based on <em>team organization</em>, <em>scalability</em> requirements, or <em>deployment</em> needs.</p><p>The difficulty isn’t writing the code. The difficulty is deciding where the boundaries truly belong.</p><p>The real problem with architectural boundaries is that almost any decision can be justified if you argue well enough.</p><p>See yourself, even with simple classes like the ones below, it’s not always obvious how they should be structured.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*851FSXZIyJFkYsvN6Pypyg.png" /></figure><p>Should you keep the interface and its implementation in the same module (<a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#67ea">N-Layered</a>)? Is it worth splitting abstractions from implementations (<a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#8386">Ports and Adapters</a>)?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TMwG8OFlaInrhLDHGBKRwg.png" /></figure><p>Would it be better if everything related to a feature lived together (<a href="https://medium.com/@iamprovidence/e3b7b8f48ce9/">Vertical Slices</a>)? Or could you isolate each module completely to avoid unnecessary references between components (<a href="https://medium.com/@iamprovidence/what-is-divine-architecture-and-why-you-dont-need-it-231008ec29b7">Divine Architecture</a>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z_Zg9sHKqxf1cJvHy0zY-Q.png" /></figure><p>The key is this: boundaries should solve a real problem — organizational, technical, or business — not exist just because you can have them.</p><p>Define a boundary whenever you need to separate responsibility, ownership, and concerns. If boundaries are well designed, the system can evolve safely and predictably. If they’re wrong, everything becomes tightly coupled and hard to change.</p><p>In any case, you probably won’t get boundaries right from the first try 😅. That’s normal. Architecture is iterative. You refine it by better understanding the domain, noticing where friction shows up, and refactoring when the pain becomes real.</p><h4>How Do You Define a Boundary?</h4><p>If you’ve decided to split your system into components instead of building one big monolith, that’s already a good sign 😅. But the moment you break a system into modules, you also need to introduce dependencies between them — and that’s where the real challenge begins. Every dependency is a form of <a href="https://medium.com/@iamprovidence/coupling-and-cohesion-foundations-that-affect-your-entire-codebase-77d06d44af0d#36ca">coupling</a>. And coupling is what determines how painful change will be.</p><p>You can try to keep dependencies manageable, enforce rules, and define clear contracts — but it’s still possible to mess things up. A few careless references, shared abstractions in the wrong place, or <a href="https://medium.com/@iamprovidence/f19688afe006">circular dependencies</a>, and your “component” starts behaving like a tangled monolith again.</p><p>That’s why it’s not enough to just split components. It’s equally important to understand the different ways to separate them properly, so boundaries stay real — and change stays cheap.</p><p><strong>— folder boundary</strong></p><p>The simplest and lightest way to organize a system is by grouping components logically with folders.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/377/1*tKXLefwSLmidCIalDtmS9w.png" /></figure><p>It gives you a clean structure with no overhead and is simple to refactor. For small systems and prototypes, this approach can work well because it keeps things fast and flexible.</p><p>But let’s be honest about the downsides 😒. There is no real enforcement — only convention. Dependencies are easy to violate, and the structure can be broken in seconds. With folders, you may not even notice when one component starts using functionality from another. Over time, this often turns into spaghetti code.</p><p><strong>— project boundary</strong></p><p>Physical separation with projects is a different thing.</p><p>The difference between a folder and a separate project is major. A folder is just an organization. A project actually gives you real boundaries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*pwG2WoSPorbUv565AhN_LA.png" /></figure><p>With separate projects, you get compile-time enforcement, clear dependency direction, explicit contracts, and better ownership.</p><p>They let you control dependencies between components, avoid <a href="https://medium.com/@iamprovidence/f19688afe006">cyclic dependencies</a>, move to a deployable NuGet library, and boundaries can be easily enforced with <a href="https://medium.com/@iamprovidence/architecture-tests-878d76e4fd56">architectural tests</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5jyJDkXA0AiM30JpqaLk5w.png" /></figure><p>Additionally, you can make use of access modifiers like public and internal that allows you to control <em>visibility</em> and <a href="https://medium.com/@iamprovidence/object-oriented-programming-078c2cb21b22#6e91"><em>encapsulation</em></a> inside an assembly. However, it only works between modules, but not within, and does not fully protect you from architectural drift.</p><p>The problem with projects is that they introduce more setup, more coordination between teams, and refactoring across projects becomes heavier. If your application grows too complex, you can still organize projects further using folders or even separate solutions. Just remember that going too far with project separation can make the codebase fragile and harder to maintain.</p><p>There are no strict rules to it. Try it yourself and see what suits you best. As always, it is a trade-off between reliability and complexity. My advice here is not to create too many projects unless you really need them.</p><p>This approach fits best for medium-to-large systems where architecture starts becoming real.</p><p><strong>— application boundary</strong></p><p>An application boundary — such as <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7">microservices</a> or N-tier — means running in a separate runtime <a href="https://medium.com/@iamprovidence/what-happens-when-you-double-click-the-exe-file-2f145ce700ad#8152">process</a>.</p><p>This provides true isolation, <a href="https://medium.com/@iamprovidence/microservices-376bb3f553b7#1771">scalability</a>, and even technology freedom. With separate processes, you can not even allow references to the wrong component.</p><p>Boundaries are enforced strictly through <a href="https://medium.com/@iamprovidence/b2e9b331fa54">API communication</a> — either inside the same machine (for example, calling another module through its public interface, IPC, <a href="https://medium.com/@iamprovidence/everything-you-always-wanted-to-know-about-cmd-powershell-bash-c0ef1871335e">CLI</a>) or over the <a href="https://medium.com/@iamprovidence/the-internet-f0db9375ee62">network</a> via <a href="https://medium.com/p/b2e9b331fa54#f12f">HTTP</a>, <a href="https://medium.com/p/b2e9b331fa54#d8d0">gRPC</a>, or <a href="https://medium.com/@iamprovidence/9cc85e8c3795/">events</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/550/1*7pPoYmJU5xrX2-SX1cft_A.png" /></figure><p>While strong internal APIs alone can dramatically reduce chaos, it is still possible to violate rules by affecting application outputs such as a database, <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84">cache</a>, queue, and so on. If components bypass those entry points and access internals directly, the boundary is meaningless.</p><p>As always, there is a cost to this level of separation, like performance degradation, serialization overhead, <a href="https://medium.com/@iamprovidence/the-internet-f0db9375ee62">network</a> complexity, distributed system challenges, versioning headaches, and operational complexity.</p><p>This level of separation should be used only when the business genuinely justifies it — not because microservices are fashionable.</p><h4>Summary</h4><p>Architecture is the art of drawing boundaries between components — and living with their consequences.</p><ul><li>A component is a cohesive part of a system that does one thing well and interacts with the rest of the system through explicit contracts.</li><li>A component can be a function, class, folder, module, project, or microservice.</li><li>There is no universally correct structure. You can separate components by: technical layers, business capabilities, rate of change, team ownership, deployment, or scalability needs. Every choice is a trade-off.</li><li>You can define boundaries logically with folders, or physically with a project or even a separate application.</li><li>Every boundary adds protection — but it also adds complexity.</li><li>Even when boundaries exist, they can still be violated. Shared databases, shared files, shared caches, or careless references can silently destroy isolation.</li><li>Boundaries are not just about structure. They require discipline within the team, clear conventions, and architectural tests.</li><li>The higher the boundary, the more expensive the mistake.<br> — at the folder level, the cost of bad boundaries is inconvenience.<br> — at the project level, it is a refactoring effort.<br> — at the service level, it becomes deployment coordination and team friction.</li><li>Separating too early can be just as harmful as not separating at all.</li><li>You won’t get boundaries perfect on the first try. Architecture is iterative. You refine it as the domain becomes clearer and friction reveals where separation truly belongs.</li><li>Sometimes poorly chosen boundaries can be more damaging than having no boundaries at all.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/768/1*HunpE3vGgsu-95deATtf7A.png" /></figure><p>Let me know in the comment section how often you organize your system 💬 Clap if you like the article👋 Support me with a coffee ☕ Follow to receive more ✅ And see you next time! 😜</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b1fafacf7a90" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unlocking the Secret Features of LINQ]]></title>
            <link>https://medium.com/@iamprovidence/unlocking-the-secret-features-of-linq-5107994b3ebf?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/5107994b3ebf</guid>
            <category><![CDATA[linq]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[csharp-programming]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Fri, 17 Apr 2026 10:01:06 GMT</pubDate>
            <atom:updated>2026-04-17T19:20:27.720Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/840/1*Daa33kwNn7zzmw7OR0-n5A.png" /></figure><p>Previously, I wrote an <a href="https://medium.com/@iamprovidence/linq-method-you-are-probably-not-aware-of-c0d36316bd2f">article</a> about a handy ‘weird’ method that prevents empty collections.</p><pre>var empty = Enumerable.Empty&lt;int&gt;();<br>var result = empty.DefaultIfEmpty(42); // [42]</pre><p>This is especially useful in aggregations to avoid exceptions or empty results.</p><pre>var maxNumber = empty<br>    .Where(n =&gt; n &gt; 25)<br>    .DefaultIfEmpty()<br>    .Max(); // no longer throw InvalidOperationException(&quot;Sequence contains no elements&quot;)</pre><p>You seem to like it 🙃.</p><p>Therefore, let’s explore a few more weird LINQ method overloads.</p><h4>Extension methods</h4><p>Let’s start with something simple, and something you already know😅.</p><p>LINQ is not magic, but just a bunch of <a href="https://medium.com/@iamprovidence/edd3ed668dfc/">extension methods</a> for IEnumerable&lt;T&gt;:</p><pre>static class Enumerable<br>{<br>                    .  .  .<br>    public static T First&lt;TSource&gt;(this IEnumerable&lt;T&gt; source);<br>    public static float? Max(this IEnumerable&lt;float?&gt; source);<br>    public static double? Sum(this IEnumerable&lt;double?&gt; source);<br>                    .  .  .<br>}</pre><p>It means two things:</p><ul><li>to make LINQ work with your own class, you need to inherit IEnumerable</li><li>while it is common to chain them, you can just call them as static methods:</li></ul><pre>var maxValue1 = new int[] { 1, 2, 3 }.Max();  // chaining<br>var maxValue2 = Enumerable.Max([1, 2, 3]);    // use a static method</pre><h4>Static LINQ Methods</h4><p>LINQ provides several handy static methods in the Enumerable class to generate sequences on the fly, without needing to manually create or fill collections.</p><pre>IEnumerable&lt;int&gt; empty  = Enumerable.Empty&lt;int&gt;();                  // []<br>IEnumerable&lt;int&gt; range  = Enumerable.Range(start: 2, count: 3);     // [2, 3, 4]<br>IEnumerable&lt;int&gt; repeat = Enumerable.Repeat(element: 27, count: 2); // [27, 27]</pre><p>And don’t forget — LINQ also offers a simple way to compare collections element-by-element 😉:</p><pre>var array1 = new[] { 1, 2, 3 };<br>var array2 = new[] { 1, 2, 4 };<br><br>var areEqual1 = Enumerable.SequenceEqual(array1, array2);<br>var areEqual2 = array1.SequenceEqual(array2);</pre><h4>.Where((value, index) =&gt; predicate)</h4><p>Many developers know popular LINQ methods — such as Select, Where, or SkipWhile. However, only a few are aware that they have overloads that accept two arguments: the <strong><em>current item</em></strong> and its <strong><em>index</em></strong>.</p><pre>var result = numbers.Where((currentItem, index) =&gt; index % 2 == 0);</pre><p>Here, x is the current element, and i is its index in the sequence.</p><h4>.FirstOrDefault()</h4><p>In all those, .FirstOrDefault(), .LastOrDefault(), .SingleOrDefault(), or .SomethingSomethingOrDefault() you can specify a<strong> </strong>default value.</p><pre>var result = numbers.FirstOrDefault(42); // returns 42 instead of default(T)</pre><p>This makes it much clearer and safer when you want a<strong><em> </em></strong><em>custom fallback value</em> instead of relying on the type’s default.</p><h4>.SelectMany()</h4><p>As you know .SelectMany() allows you to flatten multidimensional collections into a single sequence.</p><pre>var people = new[]<br>{<br>    new { Name = &quot;Alice&quot;, Pets = new[] { &quot;Dog&quot;, &quot;Cat&quot; } },<br>    new { Name = &quot;Bob&quot;,   Pets = new[] { &quot;Fish&quot; } }<br>};<br><br>// [&quot;Dog&quot;, &quot;Cat&quot;, &quot;Fish&quot;]<br>var allPets = people.SelectMany(p =&gt; p.Pets); </pre><p>But, did you know that it also has a <strong><em>second parameter </em></strong>that lets you project both the outer and inner elements into a custom shape? 🤔</p><pre>// [ { Alice - Dog }, { Alice - Cat }, { Bob - Fish } ]<br>var petOwners = people<br>  .SelectMany(<br>      p =&gt; p.Pets,                                             // collectionSelector<br>      (person, pet) =&gt; new { Owner = person.Name, Pet = pet }  // resultSelector<br>  ); </pre><h4>.Cast()</h4><p>.Cast() is useful for explicit casting, especially when working with non-generic sequences.</p><pre>List&lt;object&gt; list = new List&lt;object&gt; { 1, 2, 3 };<br><br>List&lt;int&gt; numbers = list.Cast&lt;int&gt;().ToList();</pre><p>Now, let’s say you have a base class and a derived class:</p><pre>class Animal(string Name) { }<br>class Dog(string Name, string Breed) : Animal(Name) { }</pre><p>And then work with collections of the derived type:</p><pre>IEnumerable&lt;Dog&gt; dogs = new List&lt;Dog&gt;<br>{<br>    new Dog(&quot;Buddy&quot;, &quot;Golden Retriever&quot;),<br>    new Dog(&quot;Rex&quot;, &quot;German Shepherd&quot;)<br>};</pre><p>If you want to create a collection of base entities, you can use .Cast(), but you can also convert directly to the list with .ToList&lt;BaseType&gt;():</p><pre>var animals1 = dogs.Cast&lt;Animal&gt;().ToList();<br>var animals2 = dogs.ToList&lt;Animal&gt;();<br>var animals3 = dogs.AsEnumerable&lt;Animal&gt;();</pre><h4>.GroupBy()</h4><p>.GroupBy() groups elements by a specified key.</p><pre>var people = new[]<br>{<br>    new { Name = &quot;Alice&quot;, City = &quot;London&quot; },<br>    new { Name = &quot;Bob&quot;,   City = &quot;Paris&quot; },<br>    new { Name = &quot;Carol&quot;, City = &quot;London&quot; },<br>};<br><br>// London → [{ Alice }, { Carol }]<br>// Paris  → [{ Bob }]<br>var groups = people.GroupBy(p =&gt; p.City);</pre><p>You can also specify what each group should contain using an <strong><em>element selector</em></strong>:</p><pre>// London → [&quot;Alice&quot;, &quot;Carol&quot;]<br>// Paris  → [&quot;Bob&quot;]<br>var namesByCity = people.GroupBy(p =&gt; p.City, p =&gt; p.Name);</pre><p>But did you know, you can go even further, and you can project each group into a custom result using a <strong><em>result selector</em></strong>:</p><pre>// London → { City = London, Count = 2 }<br>// Paris  → { City = Paris, Count = 1 }<br>var cityStats = people.GroupBy(<br>    p =&gt; p.City,<br>    p =&gt; p.Name,<br>    (string city, IEnumerable&lt;string&gt; names) =&gt; new <br>    { <br>        City = city, <br>        Count = names.Count(), <br>    }<br>);</pre><p>This form is especially useful when you want an aggregated result right away — like counts, averages, or summaries — without iterating over groups later.</p><h4>.Concat(), .Union(), .Zip()</h4><p>LINQ has methods that allow you to combine collections into one, let’s see the difference between those:</p><p>.Concat() allows you to <em>combine multiple sequences</em> into one. It simply appends the second sequence after the first:</p><pre>var numbers1 = new List&lt;int&gt; { 1, 2 };<br>var numbers2 = new List&lt;int&gt; { 3, 4, 5 };<br><br>var result = numbers1.Concat(numbers2).ToList(); // [1, 2, 3, 4, 5]</pre><p>.Union() is similar to .Concat(), but it <em>removes duplicates</em>, returning only distinct elements:</p><pre>var numbers1 = new List&lt;int&gt; { 1, 2, 3, 4, 5 };<br>var numbers2 = new List&lt;int&gt; { 3, 4, 5 };<br><br>var result = numbers1.Union(numbers2).ToList(); // [1, 2, 3, 4, 5]</pre><p>.Zip() is the one everybody forgot😁. It combines two sequences <em>element by element</em>, producing a new sequence based on a selector function:</p><pre>var numbers1 = new List&lt;int&gt; { 1, 2 };<br>var numbers2 = new List&lt;int&gt; { 3, 4, 5 };<br><br>var result = numbers1.Zip(numbers2).ToList(); // [(1, 3), (2, 4)]<br>var result = numbers1.Zip(numbers2, (n1, n2) =&gt; n1 * n2).ToList(); // [3, 8]</pre><p>Note: .Zip() <em>stops</em><strong> </strong>when the shorter sequence ends.</p><h4>.Aggregate()</h4><p>.Aggregate() is one of the most powerful and sometimes confusing LINQ methods. Anyone who has used it knows they’ve been through some wild times 😅.</p><p>It can be used to generalize all sorts of accumulation operations (like Sum, Max, etc).</p><pre>var numbers = new[] { 1, 2, 3, 4 };<br><br>// 10<br>var sum = numbers.Aggregate((result, currentItem) =&gt; result + currentItem);</pre><p>But it goes beyond that. Only a few know that one of its overloads allows you to set an initial value:</p><pre>// 20<br>var sum = numbers.Aggregate(<br>    10,                                             // seed<br>    (result, currentItem) =&gt; result + currentItem); // accumulation function</pre><p>And, only developers from Microsoft know (but maybe even they forget) that there is an overload that lets you transform the final accumulator before returning it:</p><pre>var words = new[] { &quot;Hello&quot;, &quot;World&quot; };<br><br>var sentence = words.Aggregate(<br>    &quot;&quot;,                                     // seed<br>    (result, word) =&gt; result + word + &quot; &quot;,  // accumulation function<br>    result =&gt; result.Trim()                 // final transformation<br>);<br><br>Console.WriteLine(sentence); // &quot;Hello World&quot;</pre><h3>Summary</h3><p>LINQ is full of surprises that few developers ever notice. So next time you write a .Where(), .Select(), or .Aggregate(), remember: there’s probably a secret overload or quirky behavior waiting to make your code a little more expressive (and maybe a little weirder 😄).</p><p>Let me know in the comment section is there&#39;s anything else I forgot about LINQ💬 Clap if you like the article👋 Support me with a coffee ☕ Follow to receive more ✅ See U! 😜</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5107994b3ebf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Infrastructure Layer: Boundaries, Best Practices, and Pitfalls]]></title>
            <link>https://medium.com/@iamprovidence/infrastructure-layer-boundaries-best-practices-and-pitfalls-2210d400db5a?source=rss-2e55517ec6ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/2210d400db5a</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-architecture]]></category>
            <dc:creator><![CDATA[iamprovidence]]></dc:creator>
            <pubDate>Tue, 07 Apr 2026 10:01:04 GMT</pubDate>
            <atom:updated>2026-04-11T15:23:51.755Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5cXGWaWXcVn-8iglmn6ybg.jpeg" /></figure><p>In today’s article, we’ll take a deep dive into the Infrastructure Layer — what it really is, why it exists, and why it’s often misunderstood. We’ll walk through how infrastructure evolved from classic N-layered systems to Hexagonal, Onion, and Clean Architecture, clarify where interfaces <em>actually</em> belong, and explore common traps like smart repositories, domain-leaking utilities, and framework-driven design.</p><p>Stick with me — this article is written to unblock juniors, challenge seniors, and hopefully save everyone a few future refactors 🙃.</p><h3>The Infrastructure Layer Explained</h3><p>The infrastructure layer is the part of an application that deals with technical and external concerns.</p><p>It is often considered the most boring layer, yet many developers — including myself — actually enjoy building technical utilities <strong>🙃</strong>.</p><p>The responsibility of this layer is to implement interactions with the outside world: <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">databases</a>, message brokers, <a href="https://medium.com/@iamprovidence/streams-8ea4b0c3cd92">file systems</a>, <a href="https://medium.com/@iamprovidence/http-client-in-c-best-practices-for-experts-840b36d8f8c4">external services</a>, <a href="https://medium.com/@iamprovidence/master-cache-in-c-307b01dc8f84">caching</a>, <a href="https://medium.com/@iamprovidence/step-by-step-guide-to-logs-in-c-best-practices-and-tips-491c8d555d26">logging</a>, and other frameworks or tools the system depends on.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TkzNpJMV2rAm9KctEtEg2g.png" /></figure><p>By isolating these details, the infrastructure layer keeps the core of the system clean, <a href="https://medium.com/@iamprovidence/unit-tests-018611590eac">testable</a>, and independent from specific technologies, allowing implementations to be replaced or evolved with minimal impact on <a href="https://medium.com/@iamprovidence/55f35ab38f45">business logic</a>.</p><h3>Where should you put Repository Interfaces?</h3><p>It took a long time for developers to understand how to properly define the infrastructure layer.</p><p>The question of where to place infrastructure interfaces and their implementations is not limited to <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">repositories</a> — it applies to all infrastructure concerns. But the database is the simplest concern to understand, therefore it will be used as an example😅.</p><p>Let’s briefly review the history to see<strong> </strong>how we got here, where these elements belong, and why we define them the way we do.</p><h4>— N-Layered</h4><p>When the <a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#67ea">N-layered</a> architecture was introduced, the infrastructure layer was very last in the line.</p><p>This made sense at the time: applications were mostly simple CRUD systems with little <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">business logic</a>, so allowing the business layer to directly access and operate on database objects was quite typical.</p><p>Additionally, infrastructure was simple back then. Typically, it was only a data access layer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eqoLAMDH9ZHcAHjPqERcXw.png" /></figure><p>Let’s have a closer look at the relationship between Business Logic and Data Access layers.</p><p><em>In N-layered architecture, both interfaces and implementations reside in the </em><strong><em>Infrastructure Layer</em></strong><em>.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1005/1*zaswAEDRT20sy6hddwQV_w.png" /></figure><p>While straightforward, trying to adapt this approach to modern tech will quickly lead to the following issues:</p><ul><li>reduced modularity — <a href="https://medium.com/@iamprovidence/55f35ab38f45">business logic</a> can’t stand alone without infrastructure assemblies</li><li>infrastructure leakage— infrastructure concepts (DB models, ORM details, <a href="https://medium.com/@iamprovidence/http-client-in-c-best-practices-for-experts-840b36d8f8c4">HTTP clients</a>) bleed into higher layers</li><li>implementation-driven interfaces — when <a href="https://medium.com/@iamprovidence/the-actual-reason-behind-interfaces-9c5aac26eb54">an interface</a> has only one implementation, it tends to mirror that implementation, leaking implementation details</li><li>fragile clients — replacing an implementation requires modifying/updating the NuGet’s version/redeploying all layers that depend on it, and not just the composition root</li><li>uncontrolled dependencies — since interfaces are public, other layers can use them, bypassing intended architectural boundaries</li></ul><h4>— Hexagon (Ports and Adapters)</h4><p><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#8386">Ports and Adapters architecture</a> applies the <a href="https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e">Dependency Inversion Principle</a>, reversing the dependency flow so infrastructure depends on <a href="https://medium.com/@iamprovidence/55f35ab38f45/">business logic</a>, not the other way around.</p><p>This becomes quite handy as applications become more complex and we start using more infrastructure like email services, <a href="https://medium.com/@iamprovidence/streams-8ea4b0c3cd92">file</a> storage, and so on.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gf2o36VVP6oV9-olh4nhEA.png" /></figure><p><em>In the Ports and Adapters architecture, infrastructure interfaces are defined alongside the </em><strong><em>Business Logic</em></strong><em>, while their implementations live in the </em><strong><em>Infrastructure Layer</em></strong><em>.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1005/1*0GsxvtCgUd9-s52ssOdF0g.png" /></figure><p>This makes stuff better and fixes issues from above, as Domain is now a central component of the system that does not have any dependencies on the Infrastructure.</p><p>However, in this architecture, the business layer often ends up containing interfaces it doesn’t really need. Ideally, the business layer should only include concepts that are meaningful to domain experts. Exposing abstractions like ICache, IMessageQueue, or IHttpClient introduces foreign infrastructure concerns, cluttering the layer.</p><p>Another problem is that your infrastructure may reference business-logic services and use them in unintended ways.</p><p>Let’s see what we have in the next architecture.</p><h4>— Onion</h4><p><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#f440">Onion Architecture</a> introduces a new layer — the Application Layer — responsible for orchestrating <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">use cases</a>, managing application behaviour like <a href="https://medium.com/@iamprovidence/all-you-need-to-know-about-task-in-c-2dce9e52c0f7">thread</a> coordination, <a href="https://medium.com/@iamprovidence/authentication-history-basic-digest-cookie-session-token-jwt-api-key-55d6c21be90b#6cc2">sessions</a>, <a href="https://medium.com/@iamprovidence/transactions-in-entity-framework-7775ab91ede1">transaction</a> boundaries, etc</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wOMqaYIjJTvaT51mA87g3w.png" /></figure><p><em>For our infrastructure, this means that interfaces can be moved into the </em><strong><em>Business</em></strong><em> or </em><strong><em>Application layers</em></strong><em>, while their implementations stay in the </em><strong><em>Infrastructure layer</em></strong><em>.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rSEndvsGjpEQmq52SwpE9g.png" /></figure><p>Repository interfaces remain in the <a href="https://medium.com/@iamprovidence/55f35ab38f45">domain layer</a>, since <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#98af">domain services </a>frequently need to access data from the database.</p><h4>— Clean Architecture</h4><p><a href="https://medium.com/@iamprovidence/backend-side-architecture-evolution-n-layered-ddd-hexagon-onion-clean-architecture-643d72444ce4#4915">Clean Architecture</a> implies that you don’t have <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#98af">domain services</a> and <a href="https://medium.com/@iamprovidence/understanding-object-oriented-design-rich-vs-anemic-models-in-c-5d8e5ea1bf39#33b0">anemic</a> models, but <a href="https://medium.com/@iamprovidence/understanding-object-oriented-design-rich-vs-anemic-models-in-c-5d8e5ea1bf39#f01b">rich</a> <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">entities</a> with both data and behaviour.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*42trt_1Udk6WoWGV2ENdSw.png" /></figure><p><em>Since we don&#39;t have services in our domain layer, all infrastructure interfaces are moved to the </em><strong><em>Application layer</em></strong><em>.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mOUq8tejMFwYO1XP7gxXBw.png" /></figure><p>Even if you define services in the business layer, data should be fetched in the application layer and all required records passed to the <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#98af">domain service</a>, emphasizing the combination of <a href="https://medium.com/@iamprovidence/ddd-trilemma-5fa726e0eef8#4972">purity and encapsulation</a>.</p><pre>class UserService : IUserService <br>{<br>    public void IsEmailFree(<br>        IReadOnlyList&lt;User&gt; users, // no DB dependency<br>        string email)<br>    {<br>        var isValid = email.Contains(&quot;@&quot;);<br>        if (!isValid) throw new InvalidArgumentException();<br><br>        var isAlreadyInUse = users.Any(u =&gt; u.Email == email);<br>        return !isAlreadyInUse;<br>    }<br>}</pre><h3>Writing Infrastructure Code</h3><p>Here are a few guidelines that can help you write better infrastructure code:</p><h4>Dependency Rules</h4><p>From the section above, you should take away the following dependency rule: <em>Infrastructure should depend on Application/Domain, never the other way around</em>.</p><p>High-level modules (Domain) should not depend on low-level modules (Infrastructure); instead, both depend on abstractions, ensuring that <a href="https://medium.com/@iamprovidence/55f35ab38f45/">business logic</a> remains unaware of concrete implementations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/529/1*O24mSUuDM2bZhZhIhQY-RQ.png" /></figure><p>This separation preserves <a href="https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e">inversion of control</a>, <a href="https://medium.com/@iamprovidence/f19688afe006/">prevents dependency cycles</a>, and allows infrastructure components to be replaced or evolved without impacting the core of the system.</p><h4>Should You Split Infrastructure?</h4><p>As already discussed, interfaces should live in the <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">application</a> or <a href="https://medium.com/@iamprovidence/55f35ab38f45/">domain layer</a>, while infrastructure stays in its own project.</p><p>Additionally, infrastructure is often grouped into <a href="https://medium.com/@iamprovidence/77d06d44af0d">cohesive</a> folders— persistence, messaging, integrations — each following <a href="https://medium.com/@iamprovidence/d96bdd6a526f">screaming architecture</a>, where the structure reflects the separate technologies it uses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/478/1*bmSe1h8KJqFuAO1HuNd7pg.png" /></figure><p>However, when infrastructure grows significantly, it can be practical to separate each implementation into its own project. The same applies to the interfaces.</p><p>Quite often, the data access layer is the first to be extracted into a separate project, as it is usually the largest and most complex part of the system.</p><p>In fact, you can extract each infrastructure per project. Having multiple small modules is what <a href="https://medium.com/@iamprovidence/77d06d44af0d">low coupling and high cohesion</a> are about:</p><p><strong>Low Coupling </strong>— refers to the degree of dependency between different components in a software system.</p><p>It is desirable to avoid dependencies for independent modules with minimal interactions.</p><p><strong>High Cohesion</strong> — refers to the degree to which the elements within a module or component are related to each other.</p><p>Classes that share the same responsibilities should belong to one module.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*YIBoRgAhyi-qI6EgSjDo6g.png" /></figure><p>Of course, it is up to you whether to keep all infrastructure in one project or to split it. The benefits of this setup are:</p><ul><li>smaller, focused projects are easier to understand, refactor, and update independently</li><li>project boundaries are stronger than folder organization. While folders cannot prevent one infrastructure module from accessing another, separate projects make these dependencies explicit and easier to control</li><li>higher-level layers never depend on concrete implementations</li><li>you can replace a concrete implementation without modifying all dependent packages (<a href="https://medium.com/@iamprovidence/solid-kiss-dry-and-other-principles-suck-55c6758322a2#b95b">open/closed principle</a>) — only the <strong><em>composition root</em></strong> needs to change (the host responsible for composing dependencies)</li><li>if your code lives in a separate NuGet, when split, implementations may be packaged separately to support multiple implementations (such as AzureMessageBus or LoopbackMessageBus) while still keeping the interface in the Domain</li><li>if parts of infrastructure evolve at different rates (e.g., database vs messaging), separate projects allow independent versioning and deployment</li><li>you are less likely to use infrastructure that you don’t need if they live in separate projects</li></ul><p>As you can see, it largely depends on the complexity of your domain, your specific needs, preferences, and future plans.</p><h4>Think about your code as if you are writing a Third-Party Library</h4><p>As your infrastructure depends on the Domain, it is natural to pass domain types across that boundary. You may feel tempted to simplify your infrastructure code and start using <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">entities</a> or domain terminology more freely.</p><pre>// Infrastructure layer<br>public class CacheService : ICacheService<br>{<br>    // ⚠️ knows about domain entities<br>    public User? GetUserById(UserId userId) { . . . }<br>    public void SetUser(User user) { . . . }<br>}</pre><p>While it is inevitable for stuff like <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">repositories</a>,<em> </em>but <em>try designing your code as if you are writing a third-party library</em>.</p><pre>// Infrastructure layer<br>public class CacheService : ICacheService<br>{<br>    // ✅ сan be packaged and reused anywhere<br>    public Task&lt;T?&gt; GetAsync&lt;T&gt;(string key) { . . . }<br>    public Task SetAsync&lt;T&gt;(string key, T value, TimeSpan ttl) { . . . }<br>}</pre><p>By doing so, your utilities (e.g. EmailService, EventBus, Cache) become independent, loosely <a href="https://medium.com/@iamprovidence/77d06d44af0d/">coupled</a>, reusable, domain-agnostic components, ideally packaged in a separate <a href="https://medium.com/@iamprovidence/what-happens-when-you-double-click-the-exe-file-2f145ce700ad#0122">library</a>.</p><h4>Make your Infrastructure Domain Agnostic</h4><p>Even with clear layering, mistakes can still happen — such as injecting <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#98af">domain services</a>, rules, or constraints into infrastructure utilities.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/803/1*isLuzchkM07OD3ckMNJuUw.png" /></figure><p>Even though, these issues are usually junior-level mistakes and are easy to spot and fix during <a href="https://medium.com/@iamprovidence/6d75c8886763/">pull request reviews</a>, the project references technically allow this.</p><p>However, this should be clear that infrastructure should remain<em> domain-agnostic</em>.</p><h4>Avoid Making Infrastructure “Smart”</h4><p>You may not inject domain rules into the Infrastructure directly, but another common mistake is giving infrastructure components responsibilities they don’t belong to — such as embedding business logic in repositories or making infrastructure “smart.”</p><pre>interface IOrderRepository<br>{<br>    OrderStatistic CalculateOrderStatistic(int orderId); // implement business layer calculation at the SQL level<br>    void UpdateStatus(int id, OrderStatus newStatus);    // update record directly, bypassing business invariants<br>}</pre><p>Repositories, message handlers, and other technical implementations should <strong><em>only perform technical tasks</em></strong>: fetching, saving, sending, or logging data.</p><p>Adding business rules or decision-making to these layers breaks <a href="https://medium.com/@iamprovidence/the-main-secret-of-software-architecture-6d153b85257c">the separation of concerns</a>, creates hidden dependencies, and makes the system harder to <a href="https://medium.com/@iamprovidence/code-testing-cdcff3c48f07">test</a> and maintain.</p><p>Keep infrastructure simple, predictable, and focused on <em>how to </em>do things, <em>not </em>what the <em>system should do</em>.</p><h4>Persistence Ignorance Principle</h4><p>The<strong><em> </em></strong><a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#8fd5"><strong><em>persistence ignorance principle</em></strong></a> implies that domain models should not know or care about how data is stored, retrieved, or persisted.</p><p>This aligns with how references are meant to work: the domain defines the rules, and infrastructure follows them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/777/1*IFhgKjKCxcc-czwMMCb3YA.png" /></figure><p>In practice, however, this principle does not always work 😅.</p><p>Persistence, as one of the most crucial concerns in a system, will indirectly affect your domain <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">entities</a>. The choice of ORM — EF, Dapper, Hibernate — shapes how you think and define your <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">entities</a> and relationships between them. This is usually not a problem as long as the influence is <em>minimal and controlled</em>.</p><pre>// Domain model <br>public class Order<br>{<br>    public Guid Id { get; private set; }<br>    // ⚠️ EF navigation properties<br>    // ⚠️ virtual used for EF<br>    public virtual ICollection&lt;OrderItem&gt; Items { get; set; } = new List&lt;OrderItem&gt;();<br><br>    // ⚠️ EF parameterless constructor<br>    protected Order() { }<br>    public Order(Guid id)<br>    {<br>        Id = id;<br>    }<br><br>    public decimal Total =&gt; Items.Sum(i =&gt; i.Price * i.Quantity);<br>}</pre><p>Your goal is to design your <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45#f3f5">domain entities</a> <strong><em>first</em></strong>, based on business meaning, rather than shaping them to make infrastructure easier to implement.</p><p>Try also applying this principle beyond persistence to other infrastructure concerns.</p><h4>Avoid Direct Infrastructure Calls From the UI</h4><p>In case your infrastructure’s interfaces are not internal and defined outside the Domain project, or you simply define layers in a different order, it becomes possible to call your infrastructure from the UI.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AUAz3t3qCwJgJQXI6f2tkg.png" /></figure><p>This is an issue because it breaks the intended dependency direction. The Presentation should only interact with the <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">Application layer</a>, never with the Infrastructure directly.</p><p>Similarly to the previous issues, it can be fixed in a few ways:</p><ul><li>establishing clear conventions and documenting allowed practices helps developers understand how layers should interact</li><li>perform <a href="https://medium.com/@iamprovidence/6d75c8886763/">code reviews</a> to enforce architecture in practice</li><li>enforce dependency rules, public/internal boundaries, and layer separation with <a href="https://medium.com/@iamprovidence/architecture-tests-878d76e4fd56">architecture tests</a></li><li>split components and define dependency in a way that your <a href="https://medium.com/@iamprovidence/what-is-divine-architecture-and-why-you-dont-need-it-231008ec29b7">supreme architecture</a> does not allow wrong usage</li><li>prevent reference propagations with project configuration</li></ul><pre>&lt;!-- Application Layer --&gt;<br>&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;<br><br>  &lt;ItemGroup&gt;<br>    &lt;ProjectReference Include=&quot;Infrastructure.Interfaces.csproj&quot;&gt;<br>      &lt;!-- Prevent Infrastructure from being implicitly accessed by other layers  --&gt;<br>      &lt;ExcludeAssets&gt;compile&lt;/ExcludeAssets&gt;<br>    &lt;/ProjectReference&gt;<br>  &lt;/ItemGroup&gt;<br><br>&lt;/Project&gt;</pre><ul><li>make interfaces internal and explicitly select trusted projects with [InternalsVisibleTo] attribute</li></ul><pre>// Infrastructure.Interfaces project<br><br>// Allow only the Application layer (or specific test projects) to access internal members<br>[assembly: InternalsVisibleTo(&quot;Application&quot;)]<br>[assembly: InternalsVisibleTo(&quot;Infrastructure.Tests&quot;)]<br><br>                          .  .  .<br><br>// Infrastructure.Implementation project<br>[assembly: InternalsVisibleTo(&quot;Presentation&quot;)] // Composition Root</pre><p>Ultimately, clean architecture is not enforced by structure alone; it requires <em>discipline</em>, reviews, and deliberate boundary protection.</p><h4>Testing Infrastructure Code</h4><p>Of course, you can mock infrastructure in <a href="https://medium.com/@iamprovidence/unit-tests-018611590eac">unit tests</a> to isolate specific behaviors and focus on verifying domain rules.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*Vr4KTGLEc_XfbQlWEmipQQ.png" /></figure><p>However, if your goal is to test Infrastructure code, you should primarily rely on <a href="https://medium.com/@iamprovidence/integration-tests-9c71f5cc5fcd"><strong><em>integration tests</em></strong></a>, since they verify interactions with external systems like databases, message brokers, or APIs.</p><h4><strong>Dockerize Your Infrastructure</strong></h4><p>Installing and configuring infrastructure components (SQL Server, Redis instance, etc) manually can be time-consuming, error-prone, and inconsistent across machines.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/134/0*nKRhnMMIQDJvq8mH.png" /></figure><p>When developing locally, it’s better to run them in <a href="https://medium.com/@iamprovidence/docker-is-not-only-for-microservices-b711ec42aaf7"><strong>Docker containers</strong> </a>rather than installing them directly on your machine to mirror production, avoid polluting your system with multiple versions, and make it easy to reset or reproduce issues.</p><h3>What Else do You Count as Infrastructure?</h3><p>For the final, let’s discuss what else counts as Infrastructure, as with some components, teams often struggle to decide where they truly belong.</p><p>Let’s briefly review common elements that are considered infrastructure details.</p><h4>— Database</h4><p>While it becomes more obvious over time that the database is nothing more than a tool, many developers still reject this and treat it as the main component of their system.</p><p>Even though it was like that in the past, technologies and requirements have shifted, but some developers’ brains have not 😬.</p><p>When the database is in the center, <a href="https://medium.com/@iamprovidence/55f35ab38f45">business logic</a>, <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">use cases</a>, and sometimes even the <a href="https://medium.com/@iamprovidence/frontend-side-architecture-evolution-mpa-spa-bd87fa32da8">UI</a> become tightly coupled to a relational schema, allowing the database to dictate application behavior.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eqoLAMDH9ZHcAHjPqERcXw.png" /></figure><p>The database may be large and dominant, but it remains just a tool — used indirectly by business rules and isolated from the core logic.</p><p>Business logic doesn’t need to know about schemas, query languages, or storage details — they only need a set of methods to load data at the beginning of a use case, and to persist it at the end for future use.</p><h4>— Frameworks</h4><p>Your architecture should be framework-independent. That framework you use should not shape your system. That means frameworks are also nothing more than just an infrastructure tool.</p><p>Frameworks commonly promote inheriting from their base classes, embedding framework functionality into business objects, and maximizing coupling to the framework.</p><pre>// Domain<br>public class User : IdentityUser&lt;Guid&gt; // ⚠️ framework dependent<br>{<br>    public string Name { get; set; }<br>}</pre><p>Heavy frameworks should be wrapped to limit their impact; otherwise, when a newer, better framework emerges, you won’t be able to adopt it without rewriting the entire system.</p><pre>// Infrastructure (ASP.NET)<br>public sealed class AspHostCurrentUser : ICurrentUser<br>{<br>                  .  .  .<br><br>    public UserId GetCurrentUserId()<br>    {<br>         return _httpContextAccessor<br>            .HttpContext<br>            .User<br>            .FindFirst(ClaimTypes.NameIdentifier);<br>    }<br>}</pre><p>The only exception I would make is <a href="https://medium.com/@iamprovidence/is-repository-an-anti-pattern-6aba7422fa48">Entity Framework</a> 😅 . However, avoid attribute-based configuration. Keep database mapping in the infrastructure layer with use of the Fluent API.</p><h4>— Host / UI</h4><p>The <a href="https://medium.com/@iamprovidence/frontend-side-architecture-evolution-mpa-spa-bd87fa32da8">user interface</a>, as the most volatile component, should be changed <a href="https://medium.com/@iamprovidence/the-main-secret-of-software-architecture-6d153b85257c">without affecting</a> the rest of the system. It should be possible to expose the same system as a <a href="https://medium.com/@iamprovidence/everything-you-always-wanted-to-know-about-cmd-powershell-bash-c0ef1871335e">console</a>, web app, desktop, or even pure <a href="https://medium.com/@iamprovidence/services-communication-http-long-polling-server-sent-events-websocket-graphql-soap-grpc-b2e9b331fa54">API</a> in a library.</p><p>How? To achieve that, you should treat your WebHost as another tool. And where do we place tools 😅? Yeap, your UI is just another infrastructure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/849/1*1o2LHyVdFbN-Ox5aaHilwA.png" /></figure><p>If so, given a diagram with Presentation and Infrastructure layers, which one should contain <em>the code</em>?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/909/1*8KfDZcrSaKa4XZxe1i7dyQ.png" /></figure><p>My advice is as follows:</p><ul><li>infrastructure code related to the UI, host configuration, presenters, and so on, can live in the <em>Presentation layer </em>(you can even create an Infrastructure folder in the host, as <a href="https://github.com/dotnet-architecture/eShopOnContainers/tree/main/src/Services/Ordering/Ordering.API/Infrastructure">Microsoft did</a>)</li><li>if you <em>need to</em> <em>reuse</em> middlewares, ActionResult or other host settings, add that to the <em>Infrastructure layer</em></li></ul><h4>— Cloud</h4><p>Cloud platforms like Azure, AWS, and Google Cloud provide many useful tools — message brokers, databases, blob storages, and more. It’s common to hide these behind <a href="https://medium.com/@iamprovidence/c-interview-questions-for-web-devs-7703f44e4e4a">interfaces</a>, keeping <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">business logic</a> abstracted while placing the concrete behavior in the infrastructure layer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/830/1*_M01RX81tDOXBfD2B1Ktpg.png" /></figure><p>However, a cloud provider can also play the role of the presentation layer. For example, Azure Functions can act as the host that starts and triggers your application. In this case, the cloud runtime is just another delivery mechanism.</p><p>At the same time, you must prevent cloud-specific details from leaking into the core of your system — for example, using cloud specific classes to send messages directly to a cloud queue from business logic.</p><pre>public static class PlaceOrderFunction<br>{<br>    [FunctionName(&quot;PlaceOrder&quot;)]<br>    public static async Task Run(<br>        [HttpTrigger(AuthorizationLevel.Function, &quot;post&quot;)]<br>        HttpRequest req,<br>        <br>        [ServiceBus(queueName: &quot;orders-queue&quot;, Connection = &quot;ServiceBusConnection&quot;)]<br>        IAsyncCollector&lt;OrderPlacedEvent&gt; output)<br>    {<br>        // ⚠️ application logic mixed with infrastructure concerns<br>        var userId = new UserId(Guid.Parse(req.HttpContext.User.Value)); // ⚠️<br><br>        var order = new Order(OrderId.New(), userId);<br>        dbContext.Orders.Add(order);<br><br>        await dbContext.SaveChangesAsync();<br>        await output.AddAsync(new OrderPlacedEvent(order.Id, userId)); // ⚠️<br>    }<br>}</pre><p>Instead, define application-level abstractions and let infrastructure handle the cloud integration.</p><pre>// Presentation Layer<br>public class PlaceOrderFunction<br>{<br>    private readonly PlaceOrderUseCase _useCase;<br><br>    public PlaceOrderFunction(PlaceOrderUseCase useCase)<br>    {<br>        _useCase = useCase;<br>    }<br>    <br>    [FunctionName(&quot;PlaceOrder&quot;)]<br>    public async Task Run(<br>        [HttpTrigger(AuthorizationLevel.Function, &quot;post&quot;)] <br>        HttpRequest req<br><br>        // ✅ do not rely on Azure Functions–specific bindings in your core logic<br>        /*IAsyncCollector&lt;OrderPlacedEvent&gt; output*/) <br>    {<br>        await _useCase.ExecuteAsync();<br>    }<br>}<br><br>------------------------------------------------------------------------------------<br><br>// Application Layer<br>public class PlaceOrderUseCase<br>{<br>    private readonly ICurrentUserService _currentUserService;<br>    private readonly IDbContext _dbContext;<br>    private readonly IMessageBus _messageBus;<br><br>    public PlaceOrderUseCase(<br>        ICurrentUserService currentUserService<br>        IDbContext dbContext,<br>        IMessageBus messageBus)<br>    {<br>        _currentUserService = currentUserService;<br>        _dbContext = dbContext;<br>        _messageBus = messageBus;<br>    }<br><br>    public async Task ExecuteAsync()<br>    {<br>        // ✅ application logic uses abstractions<br>        var userId = _currentUserService.GetCurrentUserId(); // ✅<br><br>        var order = new Order(OrderId.New(), userId);<br>        _dbContext.Orders.Add(order);<br><br>        await _dbContext.SaveChangesAsync();<br>        await _messageBus.AddAsync(new OrderPlacedEvent(order.Id, userId)); // ✅<br>    }<br>}</pre><p>From an architectural perspective, cloud-specific SDKs, configurations, and service integrations belong to the infrastructure layer and should be kept behind clear boundaries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/825/1*E8OZhepv0l1E2mc1E0l9QQ.png" /></figure><p>Treating the cloud as infrastructure preserves portability, keeps <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">business logic</a> clean, and prevents cloud providers from leaking into the core of your system.</p><h4>— Utilities</h4><p>Sometimes you write code that extends your runtime or platform like <a href="https://medium.com/@iamprovidence/edd3ed668dfc">extension methods</a>, missing data structures, or helper utilities.</p><p>Although it may seem natural to place such code in the infrastructure layer, a better choice is a <strong>Utilities</strong> layer (as long as this code is completely domain-agnostic and merely enhances your programming language capabilities):</p><pre>namespace Utilities<br>{<br>    public static class DictionaryExtensions<br>    {<br>        public static TValue GetOrDefault&lt;TKey, TValue&gt;(this IDictionary&lt;TKey, Value&gt; source, TKey key, TValue defaultValue = default!)<br>        {<br>            return source.TryGetValue(key, out var value)<br>                ? value<br>                : defaultValue;<br>        }<br>    }<br>}</pre><p>This layer can sit deeper in the stack and be reused across all other layers — <a href="https://medium.com/@iamprovidence/ddd-55f35ab38f45">domain</a>, <a href="https://medium.com/@iamprovidence/mediatr-vs-services-or-why-slices-architecture-better-2e1eb4afae43">application</a>, and host — without introducing unwanted dependencies.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Xdol070ApDKkwFJl3HtSWg.png" /></figure><h3>Summary</h3><p>The Infrastructure Layer provides concrete implementations of interfaces defined in the application or domain layers, focusing purely on technical execution, dealing with databases, external services, messaging systems, and frameworks.</p><p>Keep this code concrete, replaceable, and cohesive: implement well-defined technical behavior, avoid embedding or influencing business rules, minimize assumptions about use cases, and keep related responsibilities together.</p><p>Well-written infrastructure code is predictable, isolated, and easy to swap when technology inevitably changes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/958/1*mTN5YTPmVfkVrAu95I4kMQ.png" /></figure><p>Let me know in the comment section where do you put your infrastructure interfaces? Do you split infrastructure and implementation or keep them in the same module? What insights do you get from building utils ?💬 Clap if you like the article 👋 Support me with a coffee ☕ And follow to receive more ✅</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2210d400db5a" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>