<?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: Glacius</title>
    <description>The latest articles on DEV Community by Glacius (@glacius).</description>
    <link>https://dev.to/glacius</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3274441%2F15d5b700-1798-423c-b83f-8de344055a22.png</url>
      <title>DEV Community: Glacius</title>
      <link>https://dev.to/glacius</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/glacius"/>
    <language>en</language>
    <item>
      <title>The Art of Simplicity: A Home for Complexity</title>
      <dc:creator>Glacius</dc:creator>
      <pubDate>Wed, 27 Aug 2025 16:35:58 +0000</pubDate>
      <link>https://dev.to/glacius/the-art-of-simplicity-a-home-for-complexity-26na</link>
      <guid>https://dev.to/glacius/the-art-of-simplicity-a-home-for-complexity-26na</guid>
      <description>&lt;p&gt;In this series, we first defined simplicity as the ease of reasoning about a system. We then identified that the core complexity in modern software comes from its "non-trivial," stateful nature, where behavior is shaped by past events.&lt;/p&gt;

&lt;p&gt;To manage this, we concluded that we must define and enforce invariants—strong, unyielding rules that protect the integrity of the system's state during transitions.&lt;/p&gt;

&lt;p&gt;However, a rule described in documentation is merely a hope. This post is about turning that hope into a guarantee. We will explore how to build our most important rules into the very structure of our code, giving essential complexity a home where it is explicit, contained, and impossible to violate by design.&lt;/p&gt;

&lt;h1&gt;
  
  
  A Strong Statement
&lt;/h1&gt;

&lt;p&gt;Let’s begin with a clear and universal rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every Order always has a shipping address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we can uphold this, much of our reasoning becomes easier. Code that touches Orders no longer needs to worry about missing addresses. The rule acts as a guarantee across the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how is this typically implemented?
&lt;/h2&gt;

&lt;p&gt;When we want a rule to hold everywhere, the natural instinct is to control the path where changes enter the system. We create a narrow corridor: input is accepted at the edge, decisions are applied in the middle, and only then is state written. In most teams, that corridor takes a familiar shape: a controller receives the request, a service applies the business rules, and a repository stores the result. It’s the standard pattern we see in countless systems.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Controller–Service–Repository Architecture
&lt;/h1&gt;

&lt;p&gt;This three-layered structure has become the default way of building business applications. It shows up in frameworks, tutorials, and production systems alike. Each layer has a distinct role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Controller&lt;/strong&gt; — the entry point. It receives requests from the outside world (HTTP, messages, CLI), translates them into method calls, and returns responses. Its focus is I/O, not business rules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Service&lt;/strong&gt; — the home of application and business logic. This is where rules are applied and decisions are made. If the system has an invariant like “Every Order must always have a shipping address”, the Service is usually where that rule lives.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt; — the gateway to persistence. It hides database details behind an interface, responsible for retrieving and saving entities. It doesn’t make decisions about rules — it only stores state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these layers form a corridor: data flows from the edge inward, business rules are applied in the middle, and state is written at the bottom.&lt;/p&gt;

&lt;h1&gt;
  
  
  Implementing the Rule the Classic Way
&lt;/h1&gt;

&lt;p&gt;The requirement is clear: Every Order must always have a shipping address.&lt;br&gt;
To even talk about this rule, we need an Order—a place to hold that address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// …other fields…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is straightforward: our Order has a property for the shipping address.&lt;/p&gt;

&lt;p&gt;Next, we want to place an order. In the classical style, this logic usually lives in a Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipping address must not be empty."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the rule is enforced: the Service checks the input before creating and saving the Order. If the shipping address is missing, an exception is thrown.&lt;/p&gt;

&lt;h1&gt;
  
  
  Revisiting the Statement
&lt;/h1&gt;

&lt;p&gt;Let’s check: does our implementation uphold the invariant?&lt;/p&gt;

&lt;p&gt;The rule we want is universal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every Order always has a shipping address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our example, the only way to create an order is through the &lt;code&gt;PlaceOrder&lt;/code&gt; method on the &lt;code&gt;OrderService&lt;/code&gt;. That method explicitly enforces the rule by rejecting empty or missing addresses.&lt;/p&gt;

&lt;p&gt;So the statement that actually holds in this design is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every Order created by the PlaceOrder service method always has a shipping address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And since no other service methods exist yet, this narrower statement is equivalent to our intended universal rule. In practice, we’re safe — for now.&lt;/p&gt;

&lt;p&gt;But things rarely stay that simple.&lt;/p&gt;

&lt;h1&gt;
  
  
  A New Requirement
&lt;/h1&gt;

&lt;p&gt;Suppose the business now asks for a feature to let customers update their shipping address. We add a new method to support that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works — but notice what’s missing: there’s no validation. We’ve quietly weakened the invariant. Now an Order can exist in the database without a valid address, simply by calling this method with an empty string.&lt;/p&gt;

&lt;h1&gt;
  
  
  Patching the Rule
&lt;/h1&gt;

&lt;p&gt;We notice the hole and patch it the same way we did before — by adding validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipping address must not be empty."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now both &lt;code&gt;PlaceOrder&lt;/code&gt; and &lt;code&gt;ChangeShippingAddress&lt;/code&gt; validate shipping addresses. At first glance, the invariant seems restored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But Are We Really Safe?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not quite. Let’s revisit the statement again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every Order always has a shipping address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This still doesn’t hold universally, because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We load an existing order from the repository and simply assume it’s valid.&lt;/li&gt;
&lt;li&gt;If the database already contains an invalid Order (through old code, migrations, tests, or manual fixes), our system will happily carry it forward.&lt;/li&gt;
&lt;li&gt;Our guarantees only cover new assignments made through these service methods, not the full lifecycle of the entity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the strongest claim we can honestly make now is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every new shipping address assigned through our service methods is validated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s a much weaker statement than we started with. The invariant isn’t universal anymore — it’s conditional on how the Order was created or modified.&lt;/p&gt;

&lt;h1&gt;
  
  
  Piling On More Checks
&lt;/h1&gt;

&lt;p&gt;At this point, you might object: &lt;em&gt;“But we can catch this in code review. Or we can just be more disciplined. Or hire better engineers.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s a common argument. And in small, simple systems, it might even seem to work. A careful review process and a team that knows the rules by heart can hold things together for a while.&lt;/p&gt;

&lt;p&gt;Another objection: &lt;em&gt;“We can guard the repository too. If every update goes through a validation step there, then we’re safe, right?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But here’s the catch: none of these measures are absolute. Even with a guarded repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I can still create an &lt;code&gt;Order&lt;/code&gt; object with an empty string in a test, a migration script, or some other helper code. Nothing stops me, and nothing in the code signals that this is a violation of a core business rule.&lt;/li&gt;
&lt;li&gt;The rule has become a convention that we hope is followed, not a guarantee that is enforced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this points to the same problem: every time we discover a gap, we patch it with yet another safeguard — a check in the service, a constraint in the repository, maybe even a database trigger. Each patch makes the system feel safer, but it never truly closes the loop. Instead, the rule gets scattered into multiple places, and our confidence erodes.&lt;/p&gt;

&lt;p&gt;At some point we have to step back and ask: if invariants are so central to our system, why are we treating them as afterthoughts? Why don’t they have a single, authoritative home?&lt;/p&gt;

&lt;h1&gt;
  
  
  Where Do These Rules Really Belong?
&lt;/h1&gt;

&lt;p&gt;When we step back and look at the architecture, we often say: &lt;em&gt;“the service layer contains our business logic.”&lt;/em&gt; And at first, that sounds fine. In fact, if we inspect our &lt;code&gt;OrderService&lt;/code&gt;, it often looks like a collection of “use case” functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CreateOrder()&lt;/strong&gt;: maybe it checks that the ShippingAddress is not null.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ShipOrder()&lt;/strong&gt;: maybe it checks that the order is paid before shipping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CancelOrder()&lt;/strong&gt;: maybe it ensures you can’t cancel a shipped order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each function enforces a bit of logic. Each function is coherent on its own. But notice what’s happening here: every rule about what an Order is — rules that are meant to define the very concept of an order in our domain — are scattered across procedural guard clauses in different services.&lt;/p&gt;

&lt;p&gt;That means the statements we can make are all contextual and conditional. We can say things like &lt;em&gt;“if you create an order through this method, it will always have a shipping address.”&lt;/em&gt; But we cannot say &lt;em&gt;“an order always has a shipping address”&lt;/em&gt; — because nothing in the Order itself enforces that.&lt;/p&gt;

&lt;p&gt;Real systems rarely revolve around one rule. They’re full of invariants: minimum prices, stock limits, tax rules, user permissions, subscription states. If all of these are scattered across dozens of service methods, how can anyone — a developer, an auditor, or even a future you — know with confidence that they are always enforced?&lt;/p&gt;

&lt;p&gt;So if the service layer can’t provide those guarantees, and the repository can’t either, where should these rules live?&lt;/p&gt;

&lt;p&gt;The answer is: &lt;strong&gt;in the domain model itself&lt;/strong&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Domain Model
&lt;/h1&gt;

&lt;p&gt;Martin Fowler describes the domain model as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“An object model of the domain that incorporates both behavior and data.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, it’s not just a bag of properties to hold state. It’s a model of the problem space itself — complete with the rules, relationships, and behaviors that give that data meaning.&lt;/p&gt;

&lt;p&gt;How you visualize this depends on your architectural style:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the classic layered architecture, the domain model is a deeper layer below the services and controllers.&lt;/li&gt;
&lt;li&gt;In the hexagonal (ports and adapters) view, it’s the very core of the system, with everything else arranged around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, the point is the same: the domain model is where the concepts of your problem domain live, and it’s where the rules that define those concepts must be enforced.&lt;/p&gt;

&lt;p&gt;Seen this way, building a domain model is like creating your own little world inside the system. A world where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global invariants can be enforced.&lt;/li&gt;
&lt;li&gt;Concepts can be encoded directly into the structure of the code.&lt;/li&gt;
&lt;li&gt;Invalid states simply cannot be represented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But to make this work, we need one new meta-rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All data must go through the domain model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s the one rule that enforces all others. If every change to the system’s state passes through the model, then the model becomes the single source of truth for what is valid and what is not.&lt;/p&gt;

&lt;h1&gt;
  
  
  Trust Nothing, Verify Everything
&lt;/h1&gt;

&lt;p&gt;If this feels familiar, it should. The idea of pushing all data through the domain model has strong parallels to security principles like the principle of least privilege and zero trust.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Least privilege says: &lt;em&gt;“Never grant more access than is strictly required.”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Zero trust says: &lt;em&gt;“Never assume trust just because something is inside the system — always verify.”&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the same way, our domain model treats all data as untrusted until it passes through the rules that give it meaning. A string from a database, a JSON payload from an API, even a test fixture — none of these are considered “valid Orders” until they can be represented in the domain model.&lt;/p&gt;

&lt;p&gt;Only then does the data stop being a raw value and become part of a well-defined concept. The model itself is the gatekeeper: it either admits the data into the valid world, or rejects it as something the domain simply does not allow.&lt;/p&gt;

&lt;p&gt;That’s why we say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The domain model is the one rule to enforce all others.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Making the Rule Structural
&lt;/h1&gt;

&lt;p&gt;Let’s rewrite our &lt;code&gt;Order&lt;/code&gt;. Instead of treating it as a passive bag of data, we let it guard its own validity. The invariant is baked directly into the type itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipping address must not be empty."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipping address must not be empty."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;ShippingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the invariant is enforced in a single place — inside the domain model itself. Invalid orders can no longer be constructed, and invalid updates are impossible. The universal statement is restored:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every Order always has a shipping address.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a real application, of course, we’d also need a way to hydrate an &lt;code&gt;Order&lt;/code&gt; from stored data. That’s a practical concern for persistence — but for clarity, we’ll omit it here. The important point is that any valid Order in memory can only exist if it passes through these guards.&lt;/p&gt;

&lt;p&gt;And notice what happened to our &lt;code&gt;OrderService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ChangeShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChangeShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service no longer contains business rules. It has been reduced to orchestration — fetching data, delegating to the domain model, and persisting the results. The strong statement now lives where it belongs: in the domain itself.&lt;/p&gt;

&lt;h1&gt;
  
  
  Revisiting the Architecture
&lt;/h1&gt;

&lt;p&gt;Notice what changed in our architecture.&lt;/p&gt;

&lt;p&gt;Before, the service layer carried both application flow and the burden of enforcing core rules. That made every statement conditional on the method being used.&lt;/p&gt;

&lt;p&gt;Now the invariant lives in the domain model itself. The statement &lt;em&gt;“Every Order always has a shipping address”&lt;/em&gt; is no longer tied to a particular use case — it describes the concept of an Order universally.&lt;/p&gt;

&lt;p&gt;That frees the service layer to focus on application logic.&lt;/p&gt;

&lt;p&gt;With invariants at home in the model, the rest of the system becomes easier to trust and easier to understand. It becomes simpler.&lt;/p&gt;




&lt;p&gt;This post focused on one invariant to show why the domain model matters. In practice, domain models carry many such rules, shaping a “valid world” inside the system. Domain-Driven Design builds on this idea, offering deeper patterns for managing complexity. For now, it’s enough to see that giving invariants a home in the model is the first step toward making simplicity real.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>The Art of Simplicity: Non-Triviality in Modern Software</title>
      <dc:creator>Glacius</dc:creator>
      <pubDate>Thu, 24 Jul 2025 21:20:44 +0000</pubDate>
      <link>https://dev.to/glacius/the-art-of-simplicity-non-triviality-in-modern-software-2baj</link>
      <guid>https://dev.to/glacius/the-art-of-simplicity-non-triviality-in-modern-software-2baj</guid>
      <description>&lt;p&gt;In the previous post, we established that simplicity is about reasoning—the ability to clearly understand what a system does, how it works, and why it behaves in a particular way.&lt;/p&gt;

&lt;p&gt;To understand what impedes that reasoning, this post takes a step back to analyze the fundamental nature of the systems we build. We will borrow insights from the field of cybernetics to dissect how modern software works, on a search to diagnose what precisely makes it complex.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Non-Trivial Nature of Modern Software
&lt;/h1&gt;

&lt;p&gt;In computer science, a "Turing complete" system is capable of performing any computation a theoretical Turing machine can. All modern programming languages are Turing complete, providing the fundamental power to build incredibly diverse applications. However, while the underlying computational engine (the programming language itself, or the CPU) operates according to fixed, deterministic rules—making it "trivial" in its core operation—this doesn't mean the software systems we build using these engines are trivial.&lt;/p&gt;

&lt;p&gt;Drawing from Heinz von Foerster's work in cybernetics, we can make a crucial distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Trivial System is one whose input-output relationship is invariant and entirely predictable. Given the same input, it will always produce the exact same output, regardless of its internal state or past operations. You can precisely and analytically determine its behavior, as its rules are fixed and its history doesn't influence its current response. Think of a simple calculator: "2 + 2" always yields "4."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Non-Trivial System, conversely, is one whose input-output relationship is variant and often unpredictable from an external perspective. Its behavior depends not only on its current input but also on its internal state, which is continuously shaped by its past operations and experiences through feedback loops. Its current output is a function of both its input and its history, leading to adaptive or emergent behaviors that are hard to predict without knowing its full context.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, modern software systems have to be non-trivial to fulfill their purpose. They are designed to model and interact with the real world—processes like global economies, supply chains, or intricate biological systems—which are themselves profoundly non-trivial in their dynamic, evolving nature. Furthermore, these systems are built for and used by humans, who are the quintessential non-trivial systems. To effectively meet complex, adaptive requirements and provide meaningful interactions with users, software simply cannot remain a purely trivial, static input-output machine.&lt;/p&gt;

&lt;p&gt;Consider a common system like Identity and Access Management (IAM). When you attempt to log in, the system's response isn't a simple, fixed output for a given username and password. Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It checks its internal state (the user database) to see if your credentials exist and are valid. This state is a product of past actions, such as your initial registration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you've made too many failed attempts, the system's internal state might reflect a "locked account" status. Even with the correct password, your login will fail due to this history-dependent state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A successful login might update your "last login" timestamp, while a failed attempt might increment a "failed attempts counter." These are feedback loops where your action directly modifies the system's internal state, influencing its future behavior (e.g., triggering an alert, locking the account).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thus, the same input (your username and password) can yield different results (success, wrong password, account locked, password expired) depending on the system's internal, evolving state. This makes IAM systems, and indeed most interactive modern applications, fundamentally non-trivial. The presence of databases, user accounts, personalization features, recommendation engines, and adaptive interfaces in almost every modern application built today serves as compelling evidence: the vast majority of software systems we construct are, by their very nature, non-trivial. This inherent non-triviality fundamentally shifts how we must approach their design and comprehension.&lt;/p&gt;

&lt;h1&gt;
  
  
  Complexity in Non-Trivial Systems
&lt;/h1&gt;

&lt;p&gt;Now that we understand that modern software systems are non-trivial, what does this tell us about complexity? If simplicity is about reasoning, and non-trivial systems complicate that reasoning, then acknowledging non-triviality is the first step towards managing complexity.&lt;/p&gt;

&lt;p&gt;Consider again the definition of a trivial system: its input-output relationship is invariant, entirely predictable, and fundamentally stateless. For such a system, complexity is generally low because its behavior can be fully understood by merely examining its fixed rules and current inputs. There's little "mystery" to solve, as its past doesn't influence its present.&lt;/p&gt;

&lt;p&gt;Therefore, if most modern software is non-trivial, it follows logically that the significant complexity within these systems must lie precisely in the distinction that makes them non-trivial.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Role of Statefulness
&lt;/h1&gt;

&lt;p&gt;The defining characteristic of a non-trivial system is its internal state—a memory of past operations that evolves over time. This state is continuously modified by new inputs and, in turn, conditions all future outputs.&lt;/p&gt;

&lt;p&gt;In software, this state is pervasive, manifesting in databases, caches, session variables, and the memory of running processes. It is precisely this dynamic quality that allows an application to be personalized, persistent, and reactive, elevating it beyond a mere computational tool.&lt;/p&gt;

&lt;p&gt;Yet, this very statefulness introduces a fundamental challenge to reasoning. When a system’s output depends not just on its input but on its entire history, its behavior cannot be understood without knowing its current state and the sequence of events that produced it. The simple question, "What happens when this button is clicked?" becomes intractable, splintering into a series of context-dependent possibilities: it depends on the user's identity, their previous actions, and the state of the surrounding environment.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reinforcement Loops and Emergence
&lt;/h1&gt;

&lt;p&gt;The challenge of statefulness is further amplified by feedback and reinforcement loops. These are mechanisms where a system's output, or the consequences of that output, feed back into its internal state, influencing future behavior. In our IAM example, a failed login attempt increments a counter, which can then trigger a lockout—a clear reinforcement loop. In a recommendation engine, a user's click (output) updates their profile (state), leading to different recommendations (future output).&lt;/p&gt;

&lt;p&gt;When multiple such loops interact, especially across different components or services, predicting the system's overall behavior becomes exponentially harder. Small, local changes can propagate and amplify, leading to behaviors that were not explicitly programmed but emerge from the interaction of these loops. This emergent behavior is a hallmark of truly complex adaptive systems. It's why a small bug in a payment processing system might only manifest under very specific, rare combinations of user actions and data states, or why seemingly benign feature additions can unexpectedly degrade performance or introduce security vulnerabilities elsewhere.&lt;/p&gt;

&lt;p&gt;This phenomenon bears a striking resemblance to concepts from chaos theory, often popularized as the "butterfly effect." Just as a butterfly flapping its wings in Brazil might, over time, contribute to a hurricane in Texas, a seemingly minor change or unexpected input in one part of a highly interconnected, stateful software system can trigger a cascade of effects, leading to unforeseen and disproportionate outcomes elsewhere. This makes deterministic prediction exceptionally difficult and highlights the inherent unpredictability of highly non-trivial systems.&lt;/p&gt;

&lt;p&gt;Ultimately, this emergent nature is where the significant complexity of modern software systems truly resides. It's the source of the "labyrinth of interconnected, history-dependent decisions" we grapple with, making systems difficult to reason about, modify, and trust.&lt;/p&gt;

&lt;h1&gt;
  
  
  A Demonstration: Conway's Game of Life
&lt;/h1&gt;

&lt;p&gt;A perfect, minimalist demonstration of emergence is John Conway's Game of Life. It's not a game in the conventional sense, but a "zero-player" cellular automaton. The "universe" is a two-dimensional grid of cells, each of which can be in one of two states: alive or dead.&lt;/p&gt;

&lt;p&gt;The system evolves in discrete time steps, and its entire behavior is governed by three simple, deterministic rules applied to every cell simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Birth: A dead cell with exactly three live neighbors becomes a live cell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Survival: A live cell with two or three live neighbors survives to the next generation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Death: In all other cases, a cell dies or remains dead (due to loneliness or overcrowding).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From these trivial, local rules, an astonishing level of complexity emerges. We see patterns that are completely un-designed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Still Lifes: Stable configurations that do not change from one generation to the next.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Oscillators: Patterns that return to their original state after a finite number of generations (like the "Blinker" or "Pulsar").&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spaceships: Patterns that translate themselves across the grid (like the famous "Glider").&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, you cannot predict the long-term evolution of a complex pattern simply by looking at the rules. You have to run the simulation. The global behavior is an emergent property of the local interactions within the stateful grid. The Game of Life is a profoundly non-trivial system, where the simple, deterministic rules create a rich, unpredictable, and complex world. It's a powerful reminder of how the systems we build, even with clear rules, can produce behaviors we never explicitly intended.&lt;/p&gt;

&lt;h1&gt;
  
  
  Simplicity in the Face of Non-Triviality
&lt;/h1&gt;

&lt;p&gt;If this emergent nature is the heart of complexity, then its epicenter—the precise point where we must focus our attention—is the moment of state transition. It is the single line of code that decrements an inventory, the function that promotes a user to an admin, or the event that marks an invoice as paid. When we reason about a system's safety and predictability, we are really reasoning about the integrity of these transitions.&lt;/p&gt;

&lt;p&gt;The Art of Simplicity, therefore, is not about avoiding state, which is impossible in a non-trivial world. Instead, it is about constraining it. We must protect the integrity of our system's state by defining and rigorously enforcing invariants—strong, unyielding statements about what must always be true, especially during a transition.&lt;/p&gt;

&lt;p&gt;An invariant is the most potent form of the "strong statements" we discussed previously. It is a declaration that carves out islands of predictability in an ocean of non-triviality. Consider these invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An account balance can never become negative.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An order cannot be shipped if it has not been paid for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A user with 'read-only' permissions can never execute a 'write' operation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the load-bearing walls of our application's logic. By making them explicit and unbreakable, we gain confidence not just in what our system will do, but more importantly, in what it will never do.&lt;/p&gt;

&lt;p&gt;Acknowledging that our systems are non-trivial is the first step. Learning to define their essential invariants is the second. In the next post, we will explore how to design these into the very structure of our code, giving our system's complexity a home.&lt;/p&gt;

</description>
      <category>development</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Migrating Entities in a Poorly Designed Multi-Tenant System</title>
      <dc:creator>Glacius</dc:creator>
      <pubDate>Wed, 18 Jun 2025 18:57:59 +0000</pubDate>
      <link>https://dev.to/glacius/migrating-entities-in-a-poorly-designed-multi-tenant-system-2kcb</link>
      <guid>https://dev.to/glacius/migrating-entities-in-a-poorly-designed-multi-tenant-system-2kcb</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Multi-tenancy is powerful - but when designed poorly, even simple operations become complex.&lt;/p&gt;

&lt;p&gt;This article dives into a real-world migration challenge and how structured iteration mitigates its flaws.&lt;/p&gt;

&lt;p&gt;Finally, we’ll explore how Domain-Driven Design (DDD) could have prevented the issue entirely.&lt;/p&gt;

&lt;h1&gt;
  
  
  Understanding the Multi-Tenancy Design Flaw
&lt;/h1&gt;

&lt;p&gt;Multi-tenancy can be straightforward when done correctly. A well-structured system ensures that tenants remain logically separate, with all tenant-related data encapsulated within well-defined aggregates. However, in this case, tenant identifiers were embedded directly into almost every entity instead of being managed at the aggregate level.&lt;/p&gt;

&lt;p&gt;This led to significant architectural issues, making even basic operations like reassigning an entity to a different owner unnecessarily complex.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Consequences of This Flawed Design
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lack of Clear Boundaries:&lt;/strong&gt; \&lt;br&gt;
Instead of treating tenants as aggregates that encapsulate related entities, each entity independently tracked its own tenant ID, leading to unnecessary duplication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cascading Updates Required for Simple Changes:&lt;/strong&gt; \&lt;br&gt;
Because every entity stored its own tenant ID, moving an entity from one parent to another required updating multiple references instead of modifying ownership at the aggregate level.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Risk of Data Inconsistency:&lt;/strong&gt; \&lt;br&gt;
Without natural consistency enforcement, migrations required manual updates across multiple entities, increasing the risk of data corruption.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Breaking Down the Migration Process
&lt;/h1&gt;

&lt;p&gt;To manage this complexity, we need a structured way to identify and migrate all affected entities without hardcoding dependencies. Instead of manually tracking relationships, we break the process into two phases: &lt;strong&gt;Discovery&lt;/strong&gt; and &lt;strong&gt;Migration&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Discovery – Identifying What Needs Migration
&lt;/h2&gt;

&lt;p&gt;Before updating any references, the system must identify all entities requiring migration. This is done using Discoverers, where each entity type has a dedicated Discoverer responsible for finding and adding relevant entities to the migration process.&lt;/p&gt;

&lt;p&gt;Each Discoverer iterates over already-identified entities, detecting related entities that also require migration. Since relationships between entities may be deeply nested, a single pass is insufficient. Instead, the system repeats the discovery process until no new entities remain unprocessed - a fix-point iteration approach that dynamically accounts for all dependencies.&lt;/p&gt;

&lt;p&gt;This approach eliminates the need for predefined execution order, allowing migrations to adapt to complex relationships without hardcoded traversal logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Migration – Updating References
&lt;/h2&gt;

&lt;p&gt;Once the Discovery Phase identifies all relevant entities, the Migration Phase ensures their references are updated correctly to reflect the intended changes.&lt;/p&gt;

&lt;p&gt;To maintain consistency, Migrators apply updates to specific entity types. Each Migrator processes discovered entities and modifies their relationships according to the migration plan, ensuring that dependencies remain intact.&lt;/p&gt;

&lt;p&gt;This structured approach automates reference updates, preventing data inconsistencies and manual intervention, while keeping all dependent entities correctly linked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coordinating the Migration Process
&lt;/h2&gt;

&lt;p&gt;While Discoverers and Migrators handle specific entity types, a centralized mechanism ensures that the migration runs in a structured and repeatable manner.&lt;/p&gt;

&lt;p&gt;This is where the Orchestrator comes in. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs the Discovery Phase iteratively until no new entities are found.&lt;/li&gt;
&lt;li&gt;Executes the Migration Phase, ensuring all discovered entities are updated systematically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This structured approach is particularly valuable in real-world systems, where data models are often more complex than in simplified examples. With deeply nested relationships across multiple domains, a well-defined discovery and migration process prevents overlooked dependencies and ensures predictable, maintainable migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strengths and Limitations of This Approach
&lt;/h2&gt;

&lt;p&gt;This structured migration process provides a way to manage relationships dynamically, ensuring that all dependencies are discovered and updated systematically. However, it remains a workaround for a flawed design rather than an ideal solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strengths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No need to track execution order:&lt;/strong&gt; Fix-point iteration ensures all dependencies are discovered naturally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scales to complex real-world models:&lt;/strong&gt; Structured discovery prevents missing hidden relationships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevents inconsistencies:&lt;/strong&gt; The MigrationContext centralizes updates, reducing the risk of errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requires ongoing maintenance:&lt;/strong&gt; As the model evolves, the discovery and migration logic must be manually updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lacks natural consistency enforcement:&lt;/strong&gt; The process relies on external mechanisms rather than built-in aggregate boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adds unnecessary complexity:&lt;/strong&gt; A well-structured domain model would eliminate the need for a migration mechanism altogether.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While this approach provides structure and automation, it highlights the importance of designing systems with proper aggregate boundaries from the start. In the next section, we’ll break down how this process works in code.&lt;/p&gt;

&lt;h1&gt;
  
  
  Code Walkthrough
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Defining our Model
&lt;/h2&gt;

&lt;p&gt;Before implementing the migration process, let’s define the example data model.&lt;/p&gt;

&lt;p&gt;The diagram below represents a simplified multi-tenant structure, where each entity tracks its own tenant ID instead of inheriting it from a well-defined aggregate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LawFirm&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Other fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Clerk&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;LawFirmId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Other fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Debtor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;LawFirmId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;ClerkId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Other fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Claim&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;LawFirmId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;DebtorId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Other fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now, let’s step through the migration process and see how we can systematically discover and update affected entities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Components
&lt;/h2&gt;

&lt;p&gt;To manage the migration efficiently, the process is broken down into distinct components, each responsible for a specific aspect of discovery and updating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MigrationContext:&lt;/strong&gt; Tracks entity mappings and ensures data is updated consistently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discoverers:&lt;/strong&gt; Identify all related entities that need to be migrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrators:&lt;/strong&gt; Apply updates to maintain data consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator:&lt;/strong&gt; Coordinates the entire process iteratively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these components plays a role in ensuring a structured and predictable migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  MigrationContext: Keeping Track of Entity Mappings
&lt;/h2&gt;

&lt;p&gt;The MigrationContext serves as the central store for tracking entity relationships throughout the migration process. It performs two main tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery:&lt;/strong&gt; Keeps track of entities identified for migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping:&lt;/strong&gt; Stores source-to-destination mappings to ensure references are updated consistently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of relying on direct entity lookups, the MigrationContext dynamically maintains discovered entities and their corresponding migration targets, ensuring data consistency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MigrationContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_mappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetDiscoveredEntitiesOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;_mappings&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discoveredEntities&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;discoveredEntities&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keys&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;RegisterMapping&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;DiscoverEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsDiscovered&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_mappings&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discoveredEntitiesOfType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; 
               &lt;span class="n"&gt;discoveredEntitiesOfType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;GetDestinationEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;_mappings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetDiscoveredCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_mappings&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappings&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Discoverers: Identifying What Needs to Be Migrated
&lt;/h2&gt;

&lt;p&gt;Discoverers are responsible for identifying all entities that require migration. Each entity type has a dedicated Discoverer that ensures no dependencies are overlooked.&lt;/p&gt;

&lt;p&gt;The discovery process is iterative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It starts with already-known entities.&lt;/li&gt;
&lt;li&gt;It finds related entities that should also be migrated.&lt;/li&gt;
&lt;li&gt;The process repeats until no new entities are found - ensuring all dependencies are included dynamically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Code Example: Discovering Related Entities
&lt;/h3&gt;

&lt;p&gt;Here’s an example of a DebtorDiscoverer, which finds all debtors that belong to already-discovered clerks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DebtorDiscoverer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DebtorRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDiscoverer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MigrationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clerk&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDiscoveredEntitiesOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Clerk&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;debtors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByClerkId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;debtors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDiscovered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DiscoverEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What’s happening here?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The DebtorDiscoverer scans all already-discovered Clerks.&lt;/li&gt;
&lt;li&gt;It fetches all Debtors associated with those Clerks.&lt;/li&gt;
&lt;li&gt;If a Debtor has not yet been added to the migration context, it is discovered and registered.&lt;/li&gt;
&lt;li&gt;This process repeats iteratively until no new entities are found, ensuring all dependencies are included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach removes the need to manually define execution order - fix-point iteration ensures that all necessary entities are discovered before migration begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrators: Applying the Migration Changes
&lt;/h2&gt;

&lt;p&gt;Migrators are responsible for applying reference updates to entities once they have been identified in the discovery phase. Each entity type has a dedicated Migrator that ensures all references are updated correctly.&lt;/p&gt;

&lt;p&gt;Unlike Discoverers, which focus on identifying entities, Migrators apply structural modifications by updating foreign key relationships to reflect the new hierarchy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Example: Updating Entity References
&lt;/h3&gt;

&lt;p&gt;Here’s an example of a DebtorMigrator, which updates a Debtor’s reference to its potentially new parent entity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DebtorMigrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DebtorRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IMigrator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Migrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MigrationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDiscoveredEntitiesOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Debtor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discoveredClerkParent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDiscoveredEntitiesOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Clerk&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clerk&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClerkId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discoveredClerkParent&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;destinationClerk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDestinationEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discoveredClerkParent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LawFirmId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destinationClerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LawFirmId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClerkId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destinationClerk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debtor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What’s happening here?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The DebtorMigrator retrieves all discovered Debtors.&lt;/li&gt;
&lt;li&gt;It finds the Clerk that each Debtor is currently assigned to.&lt;/li&gt;
&lt;li&gt;If the Clerk has been migrated, the Debtor’s references are updated to reflect the new association.&lt;/li&gt;
&lt;li&gt;The updated Debtor is then saved back to the repository, ensuring persistence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By structuring the migration logic this way, we automate reference updates while ensuring that all dependent entities remain correctly linked. This method prevents inconsistencies and ensures that migrations follow a predictable and repeatable process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestrator: Running the Full Process
&lt;/h2&gt;

&lt;p&gt;The Orchestrator acts as the central coordinator of the migration process. Instead of executing rigidly ordered steps, it dynamically orchestrates both the Discovery and Migration phases to ensure a structured and reliable migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Discovery Phase: Runs iteratively, allowing discoverers to identify related entities until no new entities are found.&lt;/li&gt;
&lt;li&gt;Migration Phase: Once discovery is complete, migrators update entity references to reflect the migration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Code Example: Orchestrator Execution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDiscoverer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;discoverers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMigrator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;migrators&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Orchestrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MigrationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;newEntitiesDiscovered&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;newEntitiesDiscovered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discoverer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;discoverers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;initialCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDiscoveredCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;discoverer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDiscoveredCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;initialCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;newEntitiesDiscovered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newEntitiesDiscovered&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;migrator&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;migrators&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Migrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What’s happening here?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Discovery Phase runs iteratively until no new entities are found, ensuring all dependencies are accounted for dynamically.&lt;/li&gt;
&lt;li&gt;Each Discoverer processes known entities and identifies new related entities to be migrated.&lt;/li&gt;
&lt;li&gt;Fix-point iteration ensures completeness, meaning the process repeats until no further entities need to be migrated.&lt;/li&gt;
&lt;li&gt;Once discovery is complete, each Migrator updates entity references according to the migration plan, ensuring consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This structured execution ensures that all related entities are handled correctly without requiring hardcoded migration sequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;To migrate a set of Debtors and their Claims from one Clerk to another, we pre-populate the MigrationContext and execute the Orchestrator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;migrationContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MigrationContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;migrationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceClerk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destinationClerk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discoverers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migrators&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Orchestrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrationContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By defining what should be migrated, the system automatically discovers all dependent entities and applies the necessary updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flexible Migration Capabilities
&lt;/h3&gt;

&lt;p&gt;This system isn't limited to Clerk-level migrations. Because we can pre-populate the MigrationContext with different entity mappings, it offers some flexibility in handling migrations at different levels.&lt;/p&gt;

&lt;p&gt;For example, it could also be used to move Claims from one Debtor to another by pre-populating the context accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;migrationContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MigrationContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;migrationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceDebtor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destinationDebtor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discoverers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;migrators&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Orchestrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrationContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, all affected Claims would automatically update their references to the new Debtor and Law Firm, following the same structured discovery and migration process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flexibility Within the Given Structure
&lt;/h3&gt;

&lt;p&gt;While this example demonstrates some flexibility, the approach is still limited by the underlying model. If the relationships between entities were different or more complex, adjustments might be necessary to ensure correctness. However, within the constraints of this system, the core migration principle remains applicable to similar scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Note on This Demonstration
&lt;/h3&gt;

&lt;p&gt;This demonstration is strongly simplified to focus on the principles applied, removing unnecessary implementation details like transactions or persistence layers. In real-world scenarios, adjustments may be needed based on specific constraints and business rules.&lt;/p&gt;

&lt;p&gt;It is important to note that this is not a one-size-fits-all solution but rather a structured approach to solving a migration problem in a flawed multi-tenant system. A well-designed DDD-based architecture would have eliminated much of this complexity in the first place.&lt;/p&gt;

&lt;p&gt;For the full implementation, check out the &lt;a href="https://github.com/TheGlacius/blog-examples/tree/main/Migrating%20Entities%20in%20a%20Poorly%20Designed%20Multi-Tenant%20System" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  How It Should Have Been: The Power of DDD
&lt;/h1&gt;

&lt;p&gt;In a well-designed system, migrating Debtors between Clerks should not require a complex migration mechanism. The reason our approach was necessary is that the system was not designed with proper Aggregate Roots, forcing us to update foreign keys across multiple entities manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Issue: Scattered Tenant IDs
&lt;/h2&gt;

&lt;p&gt;The biggest design flaw in our system was that each entity (Clerk, Debtor, Claim) stored its own tenant ID, leading to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tightly coupled entities that required cascading updates during migration.&lt;/li&gt;
&lt;li&gt;No clear ownership structure, meaning relationships had to be discovered dynamically.&lt;/li&gt;
&lt;li&gt;Manual consistency enforcement, making migration error-prone and high-risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a DDD-compliant system, we would model our data in a way that ensures consistency naturally, avoiding the need for such migration logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Aggregate Roots Solve This Problem
&lt;/h2&gt;

&lt;p&gt;In Domain-Driven Design (DDD), an Aggregate Root is the single point of reference for a group of entities that should always be consistent. Instead of each entity tracking its own tenant, a single Aggregate would enforce consistency at the root level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means in practice:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Clerk owns its Debtors:&lt;/strong&gt; Debtors reference their Clerk, not the Law Firm directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Debtor owns its Claims:&lt;/strong&gt; Claims reference their Debtor, not the Law Firm directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Law Firm owns Clerks:&lt;/strong&gt; Moving a Clerk moves everything inside it automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Migration Would Work in a DDD-Based System
&lt;/h2&gt;

&lt;p&gt;In a properly designed system, migrating Debtors wouldn’t require foreign key updates across multiple tables - it would simply be a change in ownership at the Aggregate level.&lt;/p&gt;

&lt;p&gt;Here’s how the migration would work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find all Debtors belonging to the source Clerk.&lt;/li&gt;
&lt;li&gt;Reassign them to the destination Clerk.&lt;/li&gt;
&lt;li&gt;Since Claims are part of the Debtor Aggregate, they remain correctly associated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the right Aggregate boundaries, this operation is naturally consistent - no need for a Discovery Phase, no Migrators, and no complex MigrationContext.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned: Why DDD Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A well-structured model eliminates complexity:&lt;/strong&gt; When relationships are correctly modeled, data naturally stays consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate Roots provide built-in consistency:&lt;/strong&gt; Instead of relying on manual updates, changes propagate naturally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrating an Aggregate is a simple operation:&lt;/strong&gt; No need for a multi-step process, just an update at the root level.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Closing Thoughts
&lt;/h1&gt;

&lt;p&gt;This example highlights how critical it is to establish strong domain boundaries early. Without proper aggregates, even simple operations can become complex and error-prone. A well-designed system would have eliminated the need for a migration mechanism altogether.&lt;/p&gt;

&lt;p&gt;Of course, not every system starts out with the right architecture. Many businesses inherit legacy systems, making it difficult to apply DDD principles retroactively. However, recognizing these structural flaws is the first step toward improving long-term maintainability.&lt;/p&gt;

&lt;p&gt;By focusing on Aggregate Root design and clear ownership boundaries, we can prevent tenant migration problems before they happen. These principles don’t just simplify one-off migrations - they create scalable, maintainable architectures that eliminate technical debt before it accumulates.&lt;/p&gt;

&lt;p&gt;With a well-structured multi-tenancy model, migrations don’t need to be complex—they simply happen.&lt;/p&gt;

</description>
      <category>development</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Art of Simplicity: Introduction</title>
      <dc:creator>Glacius</dc:creator>
      <pubDate>Wed, 18 Jun 2025 17:29:51 +0000</pubDate>
      <link>https://dev.to/glacius/the-art-of-simplicity-what-simplicity-truly-means-47ip</link>
      <guid>https://dev.to/glacius/the-art-of-simplicity-what-simplicity-truly-means-47ip</guid>
      <description>&lt;p&gt;Software systems exist to solve problems—automating processes, managing data, enforcing logic. But simply “getting the job done” isn’t enough. Systems must also meet a wide range of requirements: performance, security, usability, maintainability, and more. The one force that threatens all of these is complexity.&lt;/p&gt;

&lt;p&gt;Complexity makes systems harder to reason about. It hides bugs, invites security holes, and slows down change. The more intricate the rules, the more expertise you need to understand or modify the system safely.&lt;/p&gt;

&lt;p&gt;In software architecture, we often define Architecture Decision Records (ADRs)—statements that explain key design decisions. Think of them as the software equivalent of legal paragraphs. And just like laws, the more exceptions and conditional logic you pile onto these statements, the harder they are to apply, enforce, or even comprehend.&lt;/p&gt;

&lt;p&gt;Consider the difference between a legal system that says “A flat 20% income tax applies to all incomes” versus one with hundreds of exceptions, exemptions, and special cases. The former can be understood at a glance. The latter requires specialists, loopholes emerge, and outcomes become unpredictable.&lt;/p&gt;

&lt;p&gt;Software is no different. When we can make broad, simple architectural statements—and enforce them consistently—we make systems easier to understand, safer to modify, and more resilient. Simplicity isn't naivety. It's a design choice, a form of discipline.&lt;/p&gt;

&lt;p&gt;This post launches a series on The Art of Simplicity—an exploration of what it is, why it matters, and how we can wield it in our systems.&lt;/p&gt;

&lt;p&gt;We start by laying the groundwork: What does simplicity truly mean?&lt;/p&gt;

&lt;h1&gt;
  
  
  What Is Simplicity?
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Simplicity is the quality or condition of being easy to understand or do.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
— Oxford Languages&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consider how you might describe a simple software system. The explanation is usually short and direct. You can walk through what it does, how its parts fit together, and why it behaves the way it does. The rules are few, and they apply consistently.&lt;/p&gt;

&lt;p&gt;Now think about describing a complex system. The explanation gets longer—full of special cases, historical decisions, and “it depends” qualifiers. The behavior varies based on context. Relationships between parts aren’t always obvious. Even small changes require careful thought and testing, because it’s not clear what might break.&lt;/p&gt;

&lt;p&gt;This is what happens as systems evolve. Features are added, exceptions pile up, and old assumptions stick around. The more of these a system accumulates, the harder it becomes to reason about its behavior—and the less simple it becomes.&lt;/p&gt;

&lt;h1&gt;
  
  
  Simplicity Is About Reasoning
&lt;/h1&gt;

&lt;p&gt;When we reason about a system, we’re asking questions and working out the answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“What happens when this button is clicked?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Which component handles this request?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Does this rule apply in this situation?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We answer these questions with statements—concise descriptions of behavior and structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“This button submits the form and shows a confirmation.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“This component processes API requests from authenticated users.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“This rule applies to invoices older than 30 days.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simpler and more consistent these statements are, the simpler the answers become. And the simpler the answers, the easier it is to reason about the system.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Legal Analogy
&lt;/h1&gt;

&lt;p&gt;Consider a legal system. At its core, it's made up of statements—rules that define what is allowed, what is required, and what happens when those rules are broken.&lt;/p&gt;

&lt;p&gt;Take a simple tax rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“A flat 20% tax applies to all income.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s  clear, easy to understand, and easy to apply. Now compare it to something more typical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“A 20% tax applies, except for incomes under 10k, students, retirees, parents, residents of certain regions, or individuals with qualifying deductions…”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This statement is more complex. As we continue adding such conditions, the rule becomes harder to interpret. Specialists are required to navigate the details. Loopholes open the door to exploitation. Mistakes become more common, and even basic decisions can be applied inconsistently.&lt;/p&gt;

&lt;p&gt;Legal systems are complex, but they serve specific functions. They answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;“Is this action legal?”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;“Who is responsible in this situation?”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;“What penalty or payment applies here?”&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To answer those questions, the system relies on clear statements (the laws) and on components (like courts or lawyers) that interpret and apply them. In many ways, this maps cleanly to a software system—where components perform specific roles, and rules define how the system behaves.&lt;/p&gt;

&lt;p&gt;The complexity of the legal system directly affects how difficult it is to answer its questions. The same is true in software. The more conditions, exceptions, and special cases we introduce, the harder it becomes to describe and reason about the system through clear statements.&lt;/p&gt;

&lt;h1&gt;
  
  
  Statements in Software
&lt;/h1&gt;

&lt;p&gt;Just like legal systems are built on rules, software systems are built on statements too. We call them different things—architecture decisions, business rules, invariants—but they serve the same purpose: to define how the system behaves.&lt;/p&gt;

&lt;p&gt;Architecture Decision Records (ADRs), for example, are formalized statements about how and why a system is designed a certain way. They capture decisions like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“We use eventual consistency between services X and Y.”&lt;/em&gt;&lt;br&gt;
   &lt;em&gt;“Authentication is handled at the API gateway.”&lt;/em&gt;&lt;br&gt;
   &lt;em&gt;“Each tenant has a logically isolated data store.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are high-level statements meant to guide understanding and align reasoning across a team.&lt;/p&gt;

&lt;p&gt;But statements don’t just live in documentation. In code, we express them most directly through tests. A test defines a setup, an action, and an expected outcome. It describes how the system should behave under a specific condition. In that way, tests are executable statements—each one both a question and an answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Given this setup, when this happens, then the system should do that.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Test-Driven Development (TDD) makes this even more explicit. It treats software development as a process of writing down expectations before writing the code that fulfills them. It's reasoning first, implementation second.&lt;/p&gt;

&lt;p&gt;In a way, tests are like an FAQ for the system. Each one answers a question: “What should happen in this scenario?” The more comprehensive and clear the tests, the easier it is to understand what the system is supposed to do—and whether it's still doing it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Strong vs. Weak Statements
&lt;/h1&gt;

&lt;p&gt;Not all statements are equal. Some are simple, clear, and easy to apply. Others are long-winded, full of edge cases, or so conditional that they lose their meaning.&lt;/p&gt;

&lt;p&gt;Strong statements tend to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Short and focused&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Free of exceptions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to verify&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Broadly applicable&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weak statements often come with qualifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;“Usually this works, unless it’s a legacy record or the feature flag is off.”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;“This service owns the data—except when it doesn’t.”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;“We validate input at the edge, unless the request comes from this internal system…”&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As complexity grows, the number of weak statements tends to increase. They overlap, contradict each other, or require mental backtracking to fully understand.&lt;/p&gt;

&lt;h1&gt;
  
  
  Can We Measure Simplicity?
&lt;/h1&gt;

&lt;p&gt;We often try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lines of code?&lt;/strong&gt; Sometimes helpful—but a short function can still be convoluted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Number of components?&lt;/strong&gt; Fewer isn’t always better if responsibilities become unclear.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Amount of functionality removed?&lt;/strong&gt; Only meaningful if the system still fulfills its purpose.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A more insightful approach is to look at the statements we can make about the system.&lt;/p&gt;

&lt;p&gt;But not just how strong they are—also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How many statements are there?&lt;/strong&gt;&lt;br&gt;
A small set of clear rules is easier to manage than a sprawling list of exceptions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How long are the statements?&lt;/strong&gt;&lt;br&gt;
Simpler systems tend to be describable in shorter, more direct terms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How many concepts are in each statement?&lt;/strong&gt;&lt;br&gt;
The more ideas packed into one sentence, the more mental unpacking is required.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How deep is the logic behind a statement?&lt;/strong&gt;&lt;br&gt;
A simple statement might hide complex branching underneath. Cyclomatic complexity increases the cost of understanding and verifying what “should” happen.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“All requests go through the API Gateway.”&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  → Strong, short, and supported by a clear structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Some requests go through the gateway, others bypass it depending on request type, environment, or legacy routing flags.”&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
  → Longer, conditional, and layered with internal knowledge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The more consistent the rules, the fewer exceptions you have to remember. The fewer moving parts in each rule, the easier it is to explain, verify, and trust.&lt;/p&gt;

&lt;h1&gt;
  
  
  Managing Complexity
&lt;/h1&gt;

&lt;p&gt;Not all complexity is bad. Some of it is necessary—it reflects the real-world domain the system is meant to model. Business rules, regulatory constraints, performance considerations: these are examples of essential complexity. They belong to the problem itself.&lt;/p&gt;

&lt;p&gt;But much of the complexity we encounter isn’t essential. It’s introduced by poor structure, vague naming, leaky abstractions, or inconsistent behavior. This is accidental complexity—complexity added by the solution rather than required by the problem.&lt;/p&gt;

&lt;p&gt;A useful guideline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Essential complexity must be understood and expressed.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accidental complexity must be found and removed.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes we reduce accidental complexity by eliminating exceptions altogether. Other times, we clarify rather than simplify—breaking complex rules into smaller, atomic ones. For example, instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Students, retirees, and parents are taxed differently…”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can separate it into distinct, verifiable statements:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“The default tax rate is 20%.”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
   &lt;em&gt;“Students are taxed at 15%.”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
   &lt;em&gt;“Retirees are taxed at 10%.”&lt;/em&gt;  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The policy hasn’t changed—but the structure has. The complexity is now expressed through independent, understandable rules. It’s easier to verify, document, and reason about.&lt;/p&gt;

&lt;p&gt;The goal is not to eliminate all complexity. It’s to make essential complexity visible—and accidental complexity disappear.&lt;/p&gt;

&lt;h1&gt;
  
  
  Abstraction: The Sharpest Tool We Have
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;“Everything should be made as simple as possible, but not simpler.”&lt;br&gt;&lt;br&gt;
— Albert Einstein&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simplicity focuses on clarity. It brings the important parts to the surface and pushes the rest into the background. The goal is not to remove complexity, but to organize it in a way that makes the system easier to understand.&lt;/p&gt;

&lt;p&gt;Abstraction is one of the main tools we use to do this. A good abstraction hides detail that isn’t relevant in the current context. It exposes a clear interface and makes behavior easier to reason about. When we use a database client, we don’t need to think about sockets or wire protocols. We just write a query. The internal complexity still exists, but it’s no longer in the way.&lt;/p&gt;

&lt;p&gt;Some abstractions go too far. When too much is hidden, we lose understanding. The system becomes unpredictable. At the extreme, an abstraction might hide everything and reveal nothing. It becomes impossible to know how or why it behaves a certain way.&lt;/p&gt;

&lt;p&gt;The opposite extreme is no abstraction at all. Every internal step, dependency, and decision is exposed. There’s no way to filter signal from noise. Complexity takes over because nothing is out of view.&lt;/p&gt;

&lt;p&gt;Both cases make reasoning harder. We need to see what matters, and ignore what doesn’t. A good abstraction helps us do that. It gives us a clear place to look—and fewer reasons to look elsewhere.&lt;/p&gt;

&lt;h1&gt;
  
  
  Simplicity Is Earned Through Understanding
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;“If you can’t explain something in simple terms, you don’t understand it.”&lt;br&gt;&lt;br&gt;
— Richard Feynman&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Programming is a form of explanation. And while it must be precise enough for machines to execute, it is written—first and foremost—for humans to understand. Programming languages exist as a shared middle ground: a format we can reason about, and a structure machines can run.&lt;/p&gt;

&lt;p&gt;When our understanding is shallow, the code reflects that. We patch gaps in our reasoning with conditionals, exceptions, and duplicated logic. These aren’t always wrong—but they often point to a missing insight.&lt;/p&gt;

&lt;p&gt;Understanding brings structure. It reveals the connections between ideas that once felt separate. In math, we learn four basic operations—addition, subtraction, multiplication, division. Later, we realize subtraction is just addition in reverse, and division is just multiplication by a reciprocal. What looked like four distinct tools turns out to be two ideas seen from different angles.&lt;/p&gt;

&lt;p&gt;The same applies to software. What seems complex often turns out to be overlapping expressions of the same underlying concept. But we only see that once we dig deep enough. Eric Evans, author of Domain-Driven Design, called this knowledge crunching—a process of exploring, refining, and eventually distilling domain knowledge until the essential structure becomes clear.&lt;/p&gt;

&lt;p&gt;That clarity enables simpler design because it starts aligning with how the domain actually works. And once that happens, simplicity follows. That’s the result of understanding.&lt;/p&gt;

&lt;h1&gt;
  
  
  Simplicity Is Complexity Handled Well
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;“Clarity trumps cleverness.”&lt;br&gt;
— John Maeda&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most real-world systems are not simple. They involve multiple concerns, constraints, and moving parts. But well-designed systems can still feel simple—because the complexity is organized, visible, and in the right place.&lt;/p&gt;

&lt;p&gt;Simplicity does not mean avoiding complexity. It means managing it deliberately—shaping it into clear concepts, placing it in the right context, and making it accessible when needed. When complexity has a defined structure, it becomes easier to understand, test, and change.&lt;/p&gt;

&lt;p&gt;The real danger lies in implicit complexity—rules and behavior that exist within the system, but are not clearly expressed. They are scattered across conditionals, duplicated in multiple places, or carried as unwritten knowledge among the team. The result is a system that feels unpredictable and fragile. The complexity is there—but no one can say exactly where.&lt;/p&gt;

&lt;p&gt;Consider a common example: a discounting system. There may be rules for seasonal promotions, loyalty rewards, referral bonuses, and more. If these rules are embedded directly in scattered if-statements across the codebase, they are difficult to change, verify, or even discuss.&lt;/p&gt;

&lt;p&gt;A clearer structure might extract this into an explicit concept—DiscountPolicy. Once named, the idea has a place in the system. It can be tested, documented, and reasoned about on its own terms.&lt;/p&gt;

&lt;p&gt;Simplicity in this case is not the absence of rules. It is the presence of clearly defined ones—located where they belong, and expressed in terms the system and its developers can both work with.&lt;/p&gt;

&lt;h1&gt;
  
  
  Simplicity Is the Ultimate Sophistication
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;“Simplicity is the ultimate sophistication.”&lt;br&gt;&lt;br&gt;
— Leonardo da Vinci&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simplicity, in its most refined form, reflects care, depth, and patience. It is shaped over time by those who have spent long enough with a system—or a problem—to see what truly matters.&lt;/p&gt;

&lt;p&gt;At first, everything seems important. The structure follows the urgency of the moment. Ideas are entangled. Rules accumulate. But as understanding grows, what once felt essential begins to fall away. What remains is clearer, more stable, more deliberate.&lt;/p&gt;

&lt;p&gt;This process is not quick. It requires attention to detail and comfort with revision. It requires a willingness to remove what no longer serves, and to arrange what remains so it can be understood without explanation.&lt;/p&gt;

&lt;p&gt;Simplicity of this kind is complete, but without excess. Everything present serves a purpose. Nothing distracts from the whole.&lt;/p&gt;

&lt;p&gt;What appears simple in the end often rests on a long history of complexity—explored, resolved, and quietly put in order. &lt;/p&gt;

&lt;p&gt;That's the Art of Simplicity.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>development</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
