<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Miroslav Thompson</title>
    <description>The latest articles on DEV Community by Miroslav Thompson (@czmirek).</description>
    <link>https://dev.to/czmirek</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957179%2F38f67c69-376c-45d3-8b8d-9750e4e8f163.jpeg</url>
      <title>DEV Community: Miroslav Thompson</title>
      <link>https://dev.to/czmirek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/czmirek"/>
    <language>en</language>
    <item>
      <title>How unclear responsibilities add to technical debt on all levels of seniority</title>
      <dc:creator>Miroslav Thompson</dc:creator>
      <pubDate>Wed, 03 Jun 2026 20:46:33 +0000</pubDate>
      <link>https://dev.to/czmirek/how-unclear-responsibilities-add-to-technical-debt-on-all-levels-of-seniority-2cao</link>
      <guid>https://dev.to/czmirek/how-unclear-responsibilities-add-to-technical-debt-on-all-levels-of-seniority-2cao</guid>
      <description>&lt;p&gt;In IT management, governance, and leadership, it is perhaps more important than anywhere else that responsibilities are clearly defined for each role.&lt;/p&gt;

&lt;p&gt;Relying on a person's seniority alone does not reveal how they will behave in situations with unclear responsibility. They will simply either &lt;strong&gt;not take&lt;/strong&gt; responsibility or &lt;strong&gt;take&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;Most engineers cannot take a firm stance in situations of uncertainty. That's why they're engineers. In a situation of unclear responsibility, they take action that aligns with their overall psychology.&lt;/p&gt;

&lt;p&gt;However, in IT, &lt;strong&gt;both&lt;/strong&gt; choices statistically lead to an increase in your applications' technical debt.&lt;/p&gt;

&lt;p&gt;Only an small fraction of people make the right choice in this situation.&lt;/p&gt;

&lt;p&gt;Before I explain what that choice is, I need to talk about why decisions regarding responsibility lead to technical debt. And before you ask: Yes, &lt;strong&gt;even taking responsibility in unclear situations increases technical debt as well!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lots of small things
&lt;/h2&gt;

&lt;p&gt;Information technology is difficult across all domains. It is a demanding field where engineers and specialists must focus on a multitude of small yet critical details, from software development to security and cloud infrastructure.&lt;/p&gt;

&lt;p&gt;Every day, the average IT engineer has to make hundreds of small choices, most of them interconnected across multiple layers. It is a mentally demanding job that requires a very high level of concentration... and we still produce bugs.&lt;/p&gt;

&lt;p&gt;Our whole field is strange like that. Doing things well requires an immense amount of focus, yet if we look away for even a second, the entire industry collapses like a house of cards in a hurricane.&lt;/p&gt;

&lt;p&gt;If engineers find themselves in a situation where it is unclear whether something is their responsibility, they will, in 99% of cases, choose to either take or not take responsibility—and you never know which it will be.&lt;/p&gt;

&lt;p&gt;They are already making a high number of decisions during a normal workday; it is simply part of their job.&lt;/p&gt;

&lt;p&gt;If you mix in uncertainty around responsibility, they will make decisions even if you, as a manager, don't want them to. This happens because the topic might actually be highly important business-wise, or it might be something that was overlooked by decision-makers. And often, it is.&lt;/p&gt;

&lt;p&gt;Instead, the engineer ends up making the decision for you. The question then becomes whether that decision aligned with the goals of the company or the product they are building. Statistically speaking, &lt;strong&gt;50% of those random decisions will not be aligned, thereby increasing technical debt.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The right action
&lt;/h2&gt;

&lt;p&gt;In a situation of uncertain responsibility, the right thing to do is to bring the issue immediately to the decision-makers. Most engineers, however, will not do that, meaning that for the average company, technical debt is inevitable.&lt;/p&gt;

&lt;p&gt;Make sure to discuss clear responsibilities with your engineers: what you expect from them, what they are currently doing, and what they should or shouldn't be doing. This is particularly relevant to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SDLC&lt;/li&gt;
&lt;li&gt;Delivery strategy&lt;/li&gt;
&lt;li&gt;Dev / DevOps / InfraOps boundaries&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>leadership</category>
      <category>management</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Ethical considerations of working with Microsoft technologies</title>
      <dc:creator>Miroslav Thompson</dc:creator>
      <pubDate>Sun, 31 May 2026 20:38:28 +0000</pubDate>
      <link>https://dev.to/czmirek/ethical-considerations-of-working-with-microsoft-technologies-3eco</link>
      <guid>https://dev.to/czmirek/ethical-considerations-of-working-with-microsoft-technologies-3eco</guid>
      <description>&lt;p&gt;I’ve been working as a C# dev since 2010. I was 16 years younger back then, and the "ethical considerations" of anything were not really a topic I was interested in. I was young and stupid, but at least smart enough (or so I thought) to see that continuing my career in PHP and its related toolchains had no future.&lt;/p&gt;

&lt;p&gt;Today, I am grappling with a question that every sane person earning a living by working with Microsoft technologies must ask: &lt;strong&gt;am I okay with this?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Big corporations are evil
&lt;/h2&gt;

&lt;p&gt;If you thought this post was not going to get political, you thought wrong.&lt;/p&gt;

&lt;p&gt;This is not a statement about Microsoft in particular, but a statement about the biggest corporations in the world.&lt;/p&gt;

&lt;p&gt;It is not, however, a statement against capitalism and the free market. The counterarguments defending capitalism and the free market, I imagine, probably come from patriotic Americans who are sick in the head.&lt;/p&gt;

&lt;p&gt;No, I’m not against capitalism and the free market—to an extent.&lt;/p&gt;

&lt;p&gt;Economics teaches us that companies try their hardest to achieve a monopoly. It’s a logical conclusion and a well-researched fact, not something we have to moralize about. Companies compete to maximize profits and long-term economic rent.&lt;/p&gt;

&lt;p&gt;But when you’re one of the biggest corporations in the world, you have more tools at your disposal. You break rules, you crush the competition, and you get involved in politics. All to achieve a monopoly. Anyone defending big corporations is either someone whose job is to do exactly that—like someone from PR—or someone insane.&lt;/p&gt;

&lt;p&gt;Have you ever met someone like that? That 25-year-old woman from HR who convinced herself that her life finally has meaning because she works for JPMorgan? She worships the brand like it’s some sort of religion. And it’s not just a professional mask; it’s her own tragically neurotic personality maintaining the brand worship in her personal life as well. And she’s not alone.&lt;/p&gt;

&lt;p&gt;On the other hand, there are people who refuse to touch anything MS-related. This was confusing to me at first—it’s software, just code, so what are you doing? What are you trying to prove?&lt;/p&gt;

&lt;p&gt;Some people do this for the wrong reasons, like refusing to touch anything that isn't open-source, or simply avoiding anything MS-related because "MS is bad" because Steve Ballmer once shat on Linux, or because Word once crashed on them.&lt;br&gt;
But those who do it for the right reasons have a point. Working with tools created by an evil entity means you are part of the problem.&lt;/p&gt;

&lt;p&gt;But... what if I build the right thing with the evil tool? What if tools made by evil can be used for good? Is it really that morally wrong to use tools made by evil?&lt;/p&gt;

&lt;p&gt;Maybe just a little. I mean, we’re in the software development world, not the weapons manufacturing industry, where the questions are much harder to ask and the answers much harder to find. I imagine someone from Heckler &amp;amp; Koch stumbling upon this article by accident, reading the heading, and chuckling at the absurdity of it.&lt;/p&gt;

&lt;p&gt;Using Visual Studio, VS Code, programming in C#... yeah, those are tools made by an evil company. Using them means we are contributing to the success of that company.&lt;/p&gt;

&lt;p&gt;Can I live with it? Yeah. But I’m not going to deny the fact that a tiny bit of my moral integrity is being suppressed for my own well-being and that of my family.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's good
&lt;/h2&gt;

&lt;p&gt;Now comes the opposite view.&lt;br&gt;
The ecosystem of MS tools is just... good. Even worse, it’s the best I’ve ever worked with. And I’ve tried a lot of different ecosystems and toolchains: PHP, C, C++, Java, Python, Node.js...&lt;/p&gt;

&lt;p&gt;Don't get me wrong, back in 2010, MS technologies sucked. We had WebForms—a website framework that was absolutely horrible to work with. Visual Studio sucked too. In 2026, it’s a completely different universe; MS sidelined the horrible black box called .NET Framework, making the new framework open-source and cross-platform.&lt;/p&gt;

&lt;p&gt;Especially when combined with Azure, the entire MS ecosystem is just great to work with, and it has no equal. People who complain about MS stuff not working just aren't seeing the side of the software development world where MS is actually doing a lot of good.&lt;/p&gt;

&lt;p&gt;It doesn’t change the fact that MS as a whole is evil—like a multi-headed hydra—but the head responsible for engineering toolchains, the ecosystem, and developer support is definitely a very good, well-behaved head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MS is evil.&lt;/strong&gt; Using tools made by MS means you’re contributing to their success. Denying this is stupid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;However, the ecosystem of tools is very good&lt;/strong&gt;—probably the best. Denying this is also stupid.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>devjournal</category>
      <category>discuss</category>
      <category>microsoft</category>
    </item>
    <item>
      <title>DevOps for Developers: Reducing Cognitive Load and Boosting Transparency</title>
      <dc:creator>Miroslav Thompson</dc:creator>
      <pubDate>Sat, 30 May 2026 20:21:20 +0000</pubDate>
      <link>https://dev.to/czmirek/devops-for-developers-reducing-cognitive-load-and-boosting-transparency-103l</link>
      <guid>https://dev.to/czmirek/devops-for-developers-reducing-cognitive-load-and-boosting-transparency-103l</guid>
      <description>&lt;p&gt;For many developers, DevOps feels like unfamiliar, overcomplicated territory filled with tangled pipelines, cloud infrastructure nuances, and containerization hysteria.&lt;/p&gt;

&lt;p&gt;But after working in this space for many years, I’ve realized that true DevOps isn't about forcing developers to become infrastructure gurus. Instead, it rests on two foundational pillars: &lt;strong&gt;maximizing transparency&lt;/strong&gt; and &lt;strong&gt;minimizing cognitive load&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Achieving this requires close collaboration between development, infrastructure, and DevOps roles.&lt;/p&gt;

&lt;p&gt;Let’s look at how to organize your code, repositories, and pipelines cleanly based on the architectural reality of what you are deploying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deployable Blocks vs. Logical Applications
&lt;/h3&gt;

&lt;p&gt;You must first understand the distinction between a deployable block and a logical application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployable Block:&lt;/strong&gt; A single, isolated technical unit that can be independently hosted or run. Examples include a single web API backend, a standalone website, a single-page application (SPA) frontend, or a native desktop app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical Application:&lt;/strong&gt; A logical grouping of multiple deployable blocks that work together to deliver a business solution. Typically, this means a backend API combined with the frontend SPA that consumes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Debugging Functional Pipelines is Extremely Time-Consuming
&lt;/h3&gt;

&lt;p&gt;Unlike developers, a DevOps engineer doesn't have a debugger to see exactly which line of code is executing in real time. To verify that a pipeline works, they must trigger the run and wait for it to complete. &lt;/p&gt;

&lt;h3&gt;
  
  
  One Repository = One Deployable Block
&lt;/h3&gt;

&lt;p&gt;Sticking multiple deployable blocks into a single repository significantly complicates your DevOps configuration. This leads to bloated, conditional pipeline configurations that are frustrating to maintain.&lt;/p&gt;

&lt;p&gt;Instead, adhere to a clean rule: &lt;strong&gt;one repository per deployable block&lt;/strong&gt;. Keep your API in its own repository and your frontend in another.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxvivs5nxweoltyll823.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxvivs5nxweoltyll823.png" alt="one repo per deployable block" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  One Pipeline Definition = One Pipeline
&lt;/h3&gt;

&lt;p&gt;One pipeline definition file must equal exactly one single pipeline. Do not write a pipeline definition that is reused across multiple deployable blocks; doing so is highly confusing and adds considerably to the total cognitive load.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Pipeline = One Responsibility
&lt;/h3&gt;

&lt;p&gt;Do not create pipeline definitions that rely on complex stage or job conditions to handle every single phase: pull requests, builds, deployments, and infrastructure provisioning. These pipelines are incredibly difficult to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;env&lt;/code&gt; Identifier
&lt;/h3&gt;

&lt;p&gt;As a DevOps engineer, you need to identify which environments you use and consistently apply their correct identifiers—often referred to simply as an &lt;code&gt;env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Individual environments should be lowercase (e.g., &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;uat&lt;/code&gt;, or &lt;code&gt;prod&lt;/code&gt;) because that is how engineers naturally speak in their daily vernacular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Which env has this bug?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Let's deploy to dev."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"How do we fix the bug on prod?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setting Up Pipelines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pipeline Naming per Responsibility
&lt;/h3&gt;

&lt;p&gt;Use this naming pattern for each pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{Deployable block name} PR&lt;/code&gt; for pull requests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{Deployable block name} BUILD&lt;/code&gt; for creating deployable artifacts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{Deployable block name} RELEASE&lt;/code&gt; for deploying to a specific environment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{Application name} INFRA&lt;/code&gt; for running IaC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always use upper-case responsibility identifiers: &lt;code&gt;PR&lt;/code&gt;, &lt;code&gt;BUILD&lt;/code&gt;, &lt;code&gt;RELEASE&lt;/code&gt;, and &lt;code&gt;INFRA&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reuse Pipeline Templates
&lt;/h3&gt;

&lt;p&gt;Even though you should strictly follow the rule of having a single pipeline definition per pipeline per responsibility, the content of each definition can remain largely identical. You can achieve this by calling a reusable template across all the applications you manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Require the &lt;code&gt;env&lt;/code&gt; for RELEASE and INFRA Pipelines
&lt;/h3&gt;

&lt;p&gt;Both &lt;code&gt;RELEASE&lt;/code&gt; and &lt;code&gt;INFRA&lt;/code&gt; pipelines require a specific target environment to execute against, whereas targeting an environment makes no sense for &lt;code&gt;PR&lt;/code&gt; and &lt;code&gt;BUILD&lt;/code&gt; pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Correct Pipeline Set
&lt;/h3&gt;

&lt;p&gt;Each deployable block has a pipeline set, but not all pipeline sets are created equal.&lt;/p&gt;

&lt;p&gt;For backend APIs or server-side web applications, the correct pipeline set is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR:&lt;/strong&gt; Runs automatically on pull requests to execute linters, check code health, and ensure the code compiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BUILD:&lt;/strong&gt; Compiles the codebase from the chosen commit into a deployable artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RELEASE:&lt;/strong&gt; Takes that exact same artifact and deploys it directly into an environment that you choose manually when running the pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, for SPA-like apps (and often native desktop apps), the situation is different. Because SPAs execute directly inside the user's browser, environment targets (like backend API connection URLs) generally must be compiled directly into the client-side bundle. Creating an environment-independent build artifact makes no sense here since the configuration is hardwired into the build itself.&lt;/p&gt;

&lt;p&gt;Therefore, you should skip a dedicated build stage. The &lt;strong&gt;RELEASE&lt;/strong&gt; pipeline does not consume a pre-compiled artifact; instead, it consumes a specific git commit directly, triggers the compilation for the chosen target environment on the fly, and deploys it immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand the INFRA Pipeline
&lt;/h3&gt;

&lt;p&gt;Application IaC (Infrastructure as Code) should not live inside individual application code repositories. Instead, maintain a separate &lt;strong&gt;Infrastructure Repository&lt;/strong&gt; that contains the &lt;code&gt;INFRA&lt;/code&gt; pipeline definition. The repository and the &lt;code&gt;INFRA&lt;/code&gt; pipeline &lt;strong&gt;cover an entire logical application, not a single deployable block.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From an infrastructure perspective, it is much easier to wire up resource configurations in IaC within a single, unified place. This allows you to orchestrate the database, backend hosting, subnets, and the networking elements required to establish secure communication inside a Zero-Trust environment. It makes little sense to split your IaC into separate, disconnected repositories per deployable unit (such as a separate infrastructure repository for the backend and another for the frontend).&lt;/p&gt;

&lt;p&gt;Like the &lt;code&gt;RELEASE&lt;/code&gt; pipeline, running the &lt;code&gt;INFRA&lt;/code&gt; pipeline strictly requires you to choose a target environment (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Run Versioning
&lt;/h3&gt;

&lt;p&gt;Azure DevOps assigns an organization-wide, non-sequential tracking ID to pipeline executions called a &lt;code&gt;Build ID&lt;/code&gt;, which is a number shared across all projects and pipelines.&lt;/p&gt;

&lt;p&gt;Instead of relying on the &lt;code&gt;Build ID&lt;/code&gt;, use a localized, incremental &lt;strong&gt;Pipeline Run Number&lt;/strong&gt; (which can be implemented using the &lt;code&gt;counter&lt;/code&gt; function in Azure DevOps). This is a simple, whole number that strictly tracks the executions of &lt;em&gt;that specific pipeline&lt;/em&gt;, starting from 1 and incrementing with each run. It is human-readable, sequential, and cognitively effortless to cross-reference. This pipeline run number becomes critical later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Pipeline Run Names, Tags, and Metadata
&lt;/h3&gt;

&lt;p&gt;At the start of a pipeline run, format the run name using a consistent pattern. For brevity, let's assume our deployable block is named &lt;code&gt;MyApp&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR pipeline:&lt;/strong&gt; &lt;code&gt;MyApp PR {PR run number}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BUILD pipeline:&lt;/strong&gt; &lt;code&gt;MyApp BUILD {BUILD run number}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RELEASE pipeline:&lt;/strong&gt; &lt;code&gt;MyApp RELEASE (BUILD {BUILD run number}) -&amp;gt; {ENV} ({RELEASE run number})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RELEASE pipeline for SPA apps:&lt;/strong&gt; &lt;code&gt;MyApp RELEASE {RELEASE run number}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INFRA pipeline:&lt;/strong&gt; &lt;code&gt;MyAppSuite INFRA {INFRA run number}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your CI/CD tool allows it, expose this information within any configurable run metadata. The &lt;code&gt;RELEASE&lt;/code&gt; pipeline naming conventions are particularly helpful for resolving a common developer complaint: &lt;em&gt;"The pipeline broke something because it deployed the wrong version."&lt;/em&gt; (More on how to disprove this below).&lt;/p&gt;




&lt;h2&gt;
  
  
  DevOps-Compliant Applications
&lt;/h2&gt;

&lt;p&gt;You cannot build a smooth delivery ecosystem if your applications operate as unreadable "black boxes". A piece of software must actively contribute to the clarity of the ecosystem as a whole.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;stdout&lt;/code&gt; Rule
&lt;/h3&gt;

&lt;p&gt;From an operational perspective, your application operates in two entirely different states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Startup Phase:&lt;/strong&gt; Everything executing &lt;em&gt;before&lt;/em&gt; the application claims a socket from the OS and begins listening on a port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Running Phase:&lt;/strong&gt; Everything executing &lt;em&gt;after&lt;/em&gt; the socket is successfully opened and request processing begins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The startup phase is critical. If your application encounters an exception here, hosting platforms will continuously spin and restart the instance in a loop. When a crash happens on startup, the runtime state of absolutely everything is unknown—including your logging libraries.&lt;/p&gt;

&lt;p&gt;You cannot rely on logging sinks (like Application Insights or Serilog) to tell you what went wrong. If the logging provider initialization itself causes the crash, it will fail silently and leave you completely blind.&lt;/p&gt;

&lt;p&gt;During the startup phase, &lt;strong&gt;write everything exclusively to standard output (&lt;code&gt;stdout&lt;/code&gt;)&lt;/strong&gt;. A pure console log is completely bulletproof. It ensures that if a boot-up crash occurs, a DevOps engineer can immediately open the container logs or cloud log stream, read the plain-text exception, and isolate the bug instantly. Save your sophisticated external telemetry sinks for the running phase after the socket is safely open.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration and secrets
&lt;/h3&gt;

&lt;p&gt;Configuring your applications should never involve logging into a production server to manually edit files. That is a recipe for configuration drift and unmitigated chaos.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Environment Variables Rule
&lt;/h4&gt;

&lt;p&gt;Every application must be built to read its configuration values directly from environment variables. Only local development machines are allowed to use files for tracking configuration changes in got depository.&lt;/p&gt;

&lt;p&gt;Cloud platforms natively expose environment variables in a highly transparent, easily adjustable UI. If you need to test a configuration change, you update the variable in the platform portal, restart the instance, and immediately see if it works.  &lt;/p&gt;

&lt;h4&gt;
  
  
  What About Secrets?
&lt;/h4&gt;

&lt;p&gt;Secrets (connection strings, API keys, etc.) are fundamentally just configuration values, but they require a Zero Trust delivery model. They absolutely do not belong in git.&lt;/p&gt;

&lt;p&gt;To handle secrets cleanly without introducing cognitive overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Masking in Variable Groups:&lt;/strong&gt; Use your CI/CD tool’s secure variable groups (like Azure DevOps Variable Groups marked with the lock icon). Once entered, they can never be viewed again by users, and the pipeline output will automatically mask them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Key Vault Reference Architecture:&lt;/strong&gt; Have your DevOps engineer set up a central Key Vault. Use a Managed Identity for your application instance, granting it strict read-only access to the vault.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Version Info Endpoint
&lt;/h2&gt;

&lt;p&gt;Every hosted deployable block should expose a public, unauthenticated JSON endpoint at &lt;code&gt;/version-info&lt;/code&gt; that returns a payload structured like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env-file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env-server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"started"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-30 13:44:51"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uptime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2d 1h 35m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildPipelineRun"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;811&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"releasePipelineRun"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;875&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"infraPipelineRun"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildPipelineUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://dev.azure.com/your-org/your-project/_build/results?buildId=13466"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"releasePipelineUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://dev.azure.com/your-org/your-project/_build/results?buildId=13799"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"infraPipelineUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://dev.azure.com/your-org/your-project/_build/results?buildId=7987"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceCommitHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceCommitBranch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The &lt;code&gt;version-info.json&lt;/code&gt; File
&lt;/h3&gt;

&lt;p&gt;Before diving into individual properties, let's look at how the &lt;code&gt;version-info.json&lt;/code&gt; file behaves.&lt;/p&gt;

&lt;p&gt;This file should live in the repository of all deployable blocks and always deploy alongside your production artifacts. The values inside the source code repository are left as empty strings or zeroes, except for &lt;code&gt;env-file&lt;/code&gt;, which defaults to the string value &lt;code&gt;local&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;env-file&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This property is explicitly overwritten by the &lt;code&gt;RELEASE&lt;/code&gt; pipeline to match the target environment selected during execution.&lt;/p&gt;

&lt;p&gt;Sometimes, developers are permitted to deploy a build directly from their local machines to lower testing environments. The presence of &lt;code&gt;"env-file": "local"&lt;/code&gt; in an environment provides an immediate visual signal that this was a local deployment, not an automated pipeline run.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;env-server&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Each deployable block running in a specific environment must have a scoped environment variable named &lt;code&gt;env&lt;/code&gt; that identifies the environment (such as &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;uat&lt;/code&gt;, or &lt;code&gt;prod&lt;/code&gt;). The &lt;code&gt;/version-info&lt;/code&gt; endpoint should surface this variable's value here.&lt;/p&gt;

&lt;p&gt;When managing a vast catalog of applications and fluid infrastructure, you aren't always in control of the underlying hostnames or DNS masks. Having the application explicitly report its active &lt;code&gt;env-server&lt;/code&gt; value gives you immediate, definitive confirmation of the environment in which the application is actually executing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;started&lt;/code&gt; and &lt;code&gt;uptime&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;These values apply primarily to server-hosted applications and are incredibly useful when diagnosing infrastructure instability or unexpected restarts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;pipelineRun&lt;/code&gt; and &lt;code&gt;pipelineUrl&lt;/code&gt; Properties
&lt;/h3&gt;

&lt;p&gt;These properties expose the localized &lt;strong&gt;pipeline run number&lt;/strong&gt; mentioned earlier.&lt;/p&gt;

&lt;p&gt;Surfacing these values will save DevOps engineers thousands of headaches. When a developer claims, &lt;em&gt;"The pipeline deployed the wrong version because my feature isn't working,"&lt;/em&gt; you can easily verify the deployment status. Simply navigate to the embedded pipeline URL, check the sequence number, identify who ran it, and correlate it directly with the running application's state.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpsBuild.net
&lt;/h2&gt;

&lt;p&gt;Would you like the idea of having a complete, opinionated dev/DevOps/InfraOps environment ready? &lt;/p&gt;

&lt;p&gt;Visit &lt;a href="https://opsbuild.net/" rel="noopener noreferrer"&gt;https://opsbuild.net/&lt;/a&gt; and subscribe to the mailing list.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Building software in C#: part 2 - code architecture</title>
      <dc:creator>Miroslav Thompson</dc:creator>
      <pubDate>Fri, 29 May 2026 19:52:31 +0000</pubDate>
      <link>https://dev.to/czmirek/building-software-in-c-part-2-code-architecture-1ofo</link>
      <guid>https://dev.to/czmirek/building-software-in-c-part-2-code-architecture-1ofo</guid>
      <description>&lt;p&gt;In my previous post, I described how historical trends shaped our (or at least my) perception of software architecture.&lt;/p&gt;

&lt;p&gt;However, with experience and seniority, you learn that code architecture always depends on a variety of context-dependent factors. This is something AI cannot help you with. A human must always establish the direction, depending on human factors such as company culture, team dynamics, formalization of work, responsibilities, and the software life cycle.&lt;/p&gt;

&lt;p&gt;The best code architecture I have encountered so far doesn't have a name yet. It is a mix of various concepts taken from different architectural patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain as a Flat List of Functions
&lt;/h2&gt;

&lt;p&gt;When you use the mediator pattern, you are basically using a service locator. Writing &lt;code&gt;CreateUserCommand&lt;/code&gt; and &lt;code&gt;CreateUserCommandHandler&lt;/code&gt; is fundamentally the same thing as writing &lt;code&gt;ICreateUserService&lt;/code&gt; and/or &lt;code&gt;CreateUserService&lt;/code&gt; with a single &lt;code&gt;CreateUser&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Ultimately, the specific approach doesn't matter. The important thing is to always &lt;strong&gt;write your domain as a flat list of functions&lt;/strong&gt;, each having a single responsibility and handling a single process. Time and again, this has proven to be the best strategy to guarantee long-term stability and, all else being equal, the avoidance of technical debt.&lt;/p&gt;

&lt;p&gt;In this article, I will use the term &lt;strong&gt;function&lt;/strong&gt;, but what I mean is a mediator command/handler or a service with a single public method; the specific shape is secondary.&lt;/p&gt;

&lt;h2&gt;
  
  
  CQRS, But Read is Part of the Domain
&lt;/h2&gt;

&lt;p&gt;Originally, CQRS was meant to separate reads and writes at the infrastructure level because, for most business applications, 90% of operations are reads and 10% are writes. Domain writes are focused on one system, and changes eventually become visible &lt;a href="https://en.wikipedia.org/wiki/Eventual_consistency" rel="noopener noreferrer"&gt;eventually&lt;/a&gt; in a separate system optimized for reading.&lt;/p&gt;

&lt;p&gt;However, I have never worked in an environment where traffic was high enough to justify such infrastructure-level optimizations. One reason is that the Czech Republic is a relatively small country with a population of around 10.5 million, and not everyone works at a high-traffic site like &lt;code&gt;seznam.cz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From a code architecture perspective, accounting for a separate read-only database is difficult. You need to produce and capture events, and you must maintain a manual or automated synchronization mechanism in case event delivery fails. Chances are, you don't need to do this at all—I certainly haven't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BUT:&lt;/strong&gt; Separating your domain functions into writing and reading functions is still an excellent idea.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;If your users interact with a UI that displays data, then reading data &lt;strong&gt;is&lt;/strong&gt; part of your domain. The DDD purists who believe the domain is strictly for writing are mistaken. The &lt;em&gt;domain&lt;/em&gt; is supposed to represent what the business needs. If the business needs to read and display data, a function that reads and returns that data belongs in the domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Higher-Order Functions
&lt;/h2&gt;

&lt;p&gt;Once you start writing your domain as a list of read or write functions, if the domain is complex enough, you will inevitably need a higher-order function: a function that calls (reuses) other functions in your domain. This is a &lt;em&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/saga" rel="noopener noreferrer"&gt;saga pattern&lt;/a&gt;&lt;/em&gt; of sorts. Your first higher-order function will be a second-level function reusing your foundational, flat list of functions.&lt;/p&gt;

&lt;p&gt;The critical architectural constraint here is that &lt;strong&gt;any higher-order function must strictly read from or write to the domain using the functions directly below them&lt;/strong&gt;. A second-level function can use the first-level (bottom) list of functions. A third-level function can only use second-level functions, and so on.&lt;/p&gt;

&lt;p&gt;Another constraint is that &lt;strong&gt;functions on the same level must NEVER invoke each other&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I have never needed anything higher than a second-level function, even in highly complex financial domains. However, in my first job, I worked on a monolithic application for travel agencies, which was probably the most complex domain I have ever seen. If that domain were written using this architecture, it would definitely require third- or even fourth-level functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monolithic Domain
&lt;/h2&gt;

&lt;p&gt;The domain is a monolith that should not be modularized within the scope of a single application or API. You should never spread domain functions across different &lt;code&gt;.csproj&lt;/code&gt; files. The only justification for moving functions elsewhere is a deliberate decision to extract a new application that takes over specific responsibilities from the original one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation, Transformation, Dependency
&lt;/h2&gt;

&lt;p&gt;These are the only three actions allowed inside your domain functions, executed in any order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; Verify whether, at any point inside the function, your in-memory data is valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation:&lt;/strong&gt; Transform or manipulate data in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency:&lt;/strong&gt; Invoke a request to an external service (including your repository) to pass data, request data, or both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Direct Use of EF Core
&lt;/h2&gt;

&lt;p&gt;Most applications I have written in my career use MSSQL as their primary data source, or at least some relational database.&lt;/p&gt;

&lt;p&gt;Time for a hard-to-swallow pill: switching from one RDBMS to another &lt;strong&gt;never happens&lt;/strong&gt;. Unless the database schema is cleanly architected from the beginning, moving from MSSQL to PostgreSQL or MySQL (or vice versa) requires an immense amount of work that is rarely justified.&lt;/p&gt;

&lt;p&gt;For those reasons, I don't see a point in abstracting the &lt;code&gt;DbContext&lt;/code&gt;. It already is the abstraction I need. The argument that you must abstract &lt;code&gt;DbContext&lt;/code&gt; into a repository interface just for testing is outdated; the ability to mock or substitute it has been solved for years. If you think EF Core is inherently slow, you probably haven't heard of &lt;code&gt;.AsNoTracking()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is also my critique of the &lt;em&gt;Unit of Work&lt;/em&gt; pattern in Clean Architecture—it is often completely useless. There is no point in abstracting &lt;code&gt;DbSet&amp;lt;User&amp;gt;.Add&lt;/code&gt; into &lt;code&gt;IUserRepository.Add&lt;/code&gt; only to invoke &lt;code&gt;IUnitOfWork.Commit&lt;/code&gt; instead of &lt;code&gt;DbContext.SaveChanges()&lt;/code&gt;. &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it" rel="noopener noreferrer"&gt;YAGNI&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies as Modules &amp;amp; Maturity Levels
&lt;/h2&gt;

&lt;p&gt;The domain needs to handle operations other than just reading and writing to the database. You invoke external APIs, write PDFs to file systems, or open sockets using legacy, third-party binary protocols.&lt;/p&gt;

&lt;p&gt;Some operations are simple enough to write directly inside the function. However, complex operations should be abstracted away into an interface (like &lt;code&gt;IInvoicePdfWriter&lt;/code&gt;) with the actual implementation moved elsewhere.&lt;/p&gt;

&lt;p&gt;Where exactly? It depends on how much architectural overhead you are willing to accept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maturity Level 5:&lt;/strong&gt; &lt;code&gt;InvoicePdfWriter&lt;/code&gt; is packaged inside a NuGet package, making it completely reusable across other projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity Level 4B:&lt;/strong&gt; &lt;code&gt;InvoicePdfWriter&lt;/code&gt; resides in a dedicated &lt;code&gt;.csproj&lt;/code&gt; file created solely for this specific functionality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity Level 4A:&lt;/strong&gt; &lt;code&gt;InvoicePdfWriter&lt;/code&gt; resides in a shared &lt;code&gt;.csproj&lt;/code&gt; for all external dependencies, organized in a dedicated folder. A custom analyzer is configured to allow or forbid dependencies the same way you would use project references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity Level 3:&lt;/strong&gt; &lt;code&gt;InvoicePdfWriter&lt;/code&gt; is in the same &lt;code&gt;.csproj&lt;/code&gt; as the domain functions, but the functions inject the &lt;code&gt;IInvoicePdfWriter&lt;/code&gt; interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity Level 2:&lt;/strong&gt; You skip the interface entirely and inject the concrete &lt;code&gt;InvoicePdfWriter&lt;/code&gt; class directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maturity level 1:&lt;/strong&gt; The dependency speficic code is written directly inside of your function. You should at least move all of that to an internal service (maturity level 2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference between 4A and 4B is practical: with 4B, you can easily end up with hundreds of &lt;code&gt;.csproj&lt;/code&gt; files even for a relatively small project. This slows down your Visual Studio instance considerably and is rarely worth the overhead.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Building software in C#: part 1 - history.</title>
      <dc:creator>Miroslav Thompson</dc:creator>
      <pubDate>Thu, 28 May 2026 20:29:09 +0000</pubDate>
      <link>https://dev.to/czmirek/building-software-in-c-part-1-history-1p1i</link>
      <guid>https://dev.to/czmirek/building-software-in-c-part-1-history-1p1i</guid>
      <description>&lt;h2&gt;
  
  
  3-Layer Architecture
&lt;/h2&gt;

&lt;p&gt;When I started programming in C# back in 2010 in Prague (Czech Republic), the dominant trend was the 3-layer monolithic architecture. This approach splits an application into a presentation layer, a business layer, and a database layer.&lt;/p&gt;

&lt;p&gt;For some reason, however, this concept was often misunderstood. Instead of layers, people treated them as independent components that could talk to each other "whenever it made sense." Senior developers during interviews would strictly insist that the business layer must sit precisely between the presentation and database layers, and that you must never communicate directly from the presentation layer to the database—or, God forbid, vice versa.&lt;/p&gt;

&lt;p&gt;The bizarre legacy projects where the UI and the database communicate directly with each other in both directions, while the "business layer" serves merely as a dumping ground for helper code "when things get too complicated," date back to this era.&lt;/p&gt;

&lt;h2&gt;
  
  
  Onion Architecture
&lt;/h2&gt;

&lt;p&gt;Some time later, the Onion Architecture technique became popular. Developers got incredibly excited about it—after all, software is never just about three simple layers, right? That would be too easy. Software is complicated, like life itself, so it needs many layers. Therefore, this had to be the correct approach.&lt;/p&gt;

&lt;p&gt;In the center of the onion is &lt;strong&gt;the core&lt;/strong&gt;—the ultimate &lt;strong&gt;root of your business&lt;/strong&gt; and the &lt;strong&gt;very center of everything&lt;/strong&gt;. Everything on top of it consists of technical layers that stack up sequentially. These outer layers aren't considered important; only the center matters.&lt;/p&gt;

&lt;p&gt;It sounded so good on paper that teams rushed to put it into practice, inadvertently creating immense technical debt for future years to come.&lt;/p&gt;

&lt;p&gt;To this day, the practical implications of the "onion" model remain vague. If you search Google Images for "onion layer architecture," you will find wildly different and conflicting interpretations. Is the center of the onion your business logic or the shape of your database entities? Can layers that touch communicate in both directions? What is the outermost layer actually supposed to be?&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain-Driven Design (DDD)
&lt;/h2&gt;

&lt;p&gt;I suspect that Domain-Driven Design (DDD) gained popularity because people were overemphasizing the importance of the Onion Architecture's "core." The "core" represented the vital part: the business logic.&lt;/p&gt;

&lt;p&gt;DDD became very explicit about this: the "domain" is the holy grail of your business. As a developer, you are expected to step back and prioritize the business logic above all else. You act as a translator of business requirements into code, and nothing more. This is DDD—business first!&lt;/p&gt;

&lt;p&gt;So, how do we implement it? Well, you need a &lt;strong&gt;domain expert&lt;/strong&gt;—a dedicated role responsible for maintaining the &lt;strong&gt;ubiquitous language&lt;/strong&gt; of the domain and translating business goals into requirements.&lt;/p&gt;

&lt;p&gt;In my entire career, I have never met anyone who officially held the title of a domain expert. Never, even in highly formalized multinational corporations, have I seen this role officially established. (Though some rumored they actually had them at Netflix).&lt;/p&gt;

&lt;p&gt;Ultimately, this approach often circled back to implementing a standard 3-layer architecture, just doing it &lt;em&gt;correctly&lt;/em&gt; this time. The domain served as the business layer, and the data flow moved strictly in one direction: from presentation to business, and then to the database. As for background processes, WSDL SOAP APIs, or webhooks—we just pretended they fit cleanly into the onion so the codebase wouldn't turn into spaghetti. Are you hungry?&lt;/p&gt;

&lt;h2&gt;
  
  
  DDD, ES, and CQRS
&lt;/h2&gt;

&lt;p&gt;For a time, this combination was the ultimate trend that suddenly made everything look pristine and perfect.&lt;/p&gt;

&lt;p&gt;You isolate your &lt;strong&gt;holy grail business core&lt;/strong&gt; into an immutable stream of changes called &lt;strong&gt;event sourcing&lt;/strong&gt; (ES). Your domain remains pure and spotless. Your classes represent domain objects in a perfect 1:1 relationship with the business, and their methods reflect real-world business processes.&lt;/p&gt;

&lt;p&gt;CQRS (Command-Query Responsibility Segregation) made the whole setup feel like an encounter with programming perfection. Commands are sent straight to your domain, while queries are handled by read models constructed asynchronously from events. Because, let’s face it, "eventual consistency" is a beautiful concept.&lt;/p&gt;

&lt;p&gt;It worked beautifully until someone asked a fundamental question: &lt;em&gt;"How do we ensure user email addresses are unique?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This simple requirement made some architects so defensive they argued back: &lt;em&gt;"That's a stupid requirement, you don't actually need unique emails, go away."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But seriously, how do you handle this in a pure DDD/ES/CQRS setup?&lt;/p&gt;

&lt;p&gt;You cannot easily enforce it inside a &lt;code&gt;User&lt;/code&gt; class because that class only knows the context of a single user. Do you create a &lt;code&gt;UserCollection&lt;/code&gt; class in front of your commands to manage emails? If so, how does a &lt;code&gt;UserCollection&lt;/code&gt; capture the meaningful business knowledge that DDD advocates so strongly for?&lt;/p&gt;

&lt;p&gt;Furthermore, how do you validate uniqueness across a collection if you rely on event sourcing to load your domain state? Loading every single historical email into memory to run a check is clearly inefficient. You are left with no choice but to store user emails separately, outside of the primary event store.&lt;/p&gt;

&lt;p&gt;This highlights the biggest drawback of the DDD/ES/CQRS paradigm: extreme complexity. Managing snapshots, command/event versioning, data anonymization (for GDPR) within the event store, event ordering, concurrency, and read-model desynchronization means you often spend more time engineering a starship than shipping production-ready features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microservices!
&lt;/h2&gt;

&lt;p&gt;Containers! Kubernetes!&lt;/p&gt;

&lt;p&gt;This wasn't really a code architecture trend but infrastructure one, but it affected our perception of how we should build things. Splitting everything into separate service solidified the idea of strong domain. Now every individual domain part has its own application that communicates with other parts very tightly over the network.&lt;/p&gt;

&lt;p&gt;At around the same time GRPC was popularized to make the network communication more efficient.&lt;/p&gt;

&lt;p&gt;This was such a strong trend it made everyone think on how their applications can be split into microservices, without considering the costs of doing it and the costs of maintaining it.&lt;/p&gt;

&lt;h2&gt;
  
  
  MediatR
&lt;/h2&gt;

&lt;p&gt;Not long ago, MediatR and the mediator pattern gained massive popularity in the C# ecosystem. Suddenly, applications were structured around commands, notifications, and interfaces like &lt;code&gt;IMediator&lt;/code&gt;, &lt;code&gt;ISender&lt;/code&gt;, or &lt;code&gt;IPublisher&lt;/code&gt; to build request pipelines.&lt;/p&gt;

&lt;p&gt;Developers, likely exhausted by the overhead of DDD/ES/CQRS, looked for something simpler.&lt;/p&gt;

&lt;p&gt;MediatR helped many realize that treating the "domain" as a flat list of distinct, single units of work is highly maintainable, predictable, and clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean Architecture
&lt;/h2&gt;

&lt;p&gt;Looking at our industry's history, the name "Clean Architecture" itself highlights how deeply developers crave perfection. It often feels like the field is driven by an ongoing effort to prove architectural points.&lt;/p&gt;

&lt;p&gt;In reality, Clean Architecture is essentially a modernized 3-layer architecture split across five or more projects. It leverages the mediator pattern and offers a structured separation of concerns that—once you gain experience with it—makes it highly obvious where every piece of code belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vertical Slicing
&lt;/h2&gt;

&lt;p&gt;Vertical slicing shifts the focus toward the componentization of your code. Instead of organizing your project by technical layers, you group code by distinct business features, ensuring each folder contains all the layers relevant to that specific functionality.&lt;/p&gt;

&lt;p&gt;For example, instead of scattering a feature across a &lt;code&gt;UserRepository&lt;/code&gt; (in a data project), a &lt;code&gt;UserService&lt;/code&gt; (in a business project), and a &lt;code&gt;UserPage.razor&lt;/code&gt; (in a UI project), you place them all into a single &lt;code&gt;User&lt;/code&gt; folder. Everything related to users lives in one place.&lt;/p&gt;

&lt;p&gt;This approach delivers excellent modularity. It is arguably one of the best ways to build single-server web applications or UIs, though it requires careful boundary management when designing complex APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modular monolith
&lt;/h2&gt;

&lt;p&gt;At one point we found out that blindly building everything around microservices and containerizing everything is not a good idea and that running a monolith, for majority of companies out there, is cheaper, sane and simply the right decision.&lt;/p&gt;

&lt;p&gt;But this time we know there are much better approaches to code, such as vertical slicing, but we're calling them modules now.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
