<?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: Rodrigo de Oliveira</title>
    <description>The latest articles on DEV Community by Rodrigo de Oliveira (@rodri-oliveira-dev).</description>
    <link>https://dev.to/rodri-oliveira-dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4130418%2F5f4d2175-a0f9-4e03-a62e-b636a7fca764.jpeg</url>
      <title>DEV Community: Rodrigo de Oliveira</title>
      <link>https://dev.to/rodri-oliveira-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rodri-oliveira-dev"/>
    <language>en</language>
    <item>
      <title>Dapper FluentMap Is Back: What Changed from Version 2.0 to 3.6?</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/dapper-fluentmap-is-back-what-changed-from-version-20-to-36-em8</link>
      <guid>https://dev.to/rodri-oliveira-dev/dapper-fluentmap-is-back-what-changed-from-version-20-to-36-em8</guid>
      <description>&lt;p&gt;&lt;em&gt;After six years without a new stable release, Dapper FluentMap is actively maintained again. But bringing an existing library back to life involves more than updating dependencies. It means finding a way to move forward without breaking the contracts that existing applications rely on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine a .NET application that uses Dapper to query a legacy database. In the code, we have a &lt;code&gt;Customer&lt;/code&gt; entity with properties such as &lt;code&gt;Id&lt;/code&gt; and &lt;code&gt;Name&lt;/code&gt;. In the database, the columns follow a different naming convention: &lt;code&gt;customer_id&lt;/code&gt; and &lt;code&gt;customer_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Handling this difference is not particularly difficult. We can use SQL aliases, introduce intermediate objects, or configure explicit mappings. All are reasonable approaches, depending on the context.&lt;/p&gt;

&lt;p&gt;The problem becomes more noticeable when these differences spread across dozens of queries and entities. Naming conventions, legacy database structures, and persistence-specific decisions gradually find their way into the application code.&lt;/p&gt;

&lt;p&gt;Dapper FluentMap was originally created to address this kind of problem: mapping .NET properties to database columns without introducing persistence attributes into domain entities.&lt;/p&gt;

&lt;p&gt;In August 2020, version 2.0.0 was released, updating its integration with Dapper 2.0.35 and Dommel 2.0. After that, the project went through a long period without new stable releases.&lt;/p&gt;

&lt;p&gt;In 2026, I took over its maintenance.&lt;/p&gt;

&lt;p&gt;The first question wasn't how many features could be added. It was how to evolve an established library without disrupting applications that might still depend on it.&lt;/p&gt;

&lt;p&gt;Today, with version 3.6.0, released on October 13, 2026, FluentMap retains its original purpose while introducing capabilities that were not available in the 2.x line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Decision: Preserve What Already Worked
&lt;/h2&gt;

&lt;p&gt;A general-purpose library carries a different kind of responsibility than an application maintained by a single team.&lt;/p&gt;

&lt;p&gt;When we change an application, we usually know its consumers and can coordinate a migration. With a public library, we don't necessarily know how many projects depend on a particular behavior or which architectural decisions were made around it.&lt;/p&gt;

&lt;p&gt;For that reason, I see compatibility as an architectural requirement, not merely a versioning concern.&lt;/p&gt;

&lt;p&gt;In FluentMap, the traditional configuration model remains available:&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;Customer&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="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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerMap&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EntityMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;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;CustomerMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&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="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer_name"&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;Registration remains familiar:&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="n"&gt;FluentMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerMap&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;FluentMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And queries still use Dapper directly:&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;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QuerySingle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"SELECT customer_id, customer_name FROM customers WHERE customer_id = @Id"&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="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FluentMap establishes the relationship between column names and entity properties. SQL execution, connection management, and basic materialization remain Dapper's responsibility.&lt;/p&gt;

&lt;p&gt;Internally, this integration relies on Dapper's type-mapping mechanism, connected to FluentMap's global configuration.&lt;/p&gt;

&lt;p&gt;That behavior has been preserved in the 3.x line.&lt;/p&gt;

&lt;p&gt;For applications using only traditional mappings, upgrading generally does not require rewriting entities or replacing existing query calls. This does not mean unrestricted compatibility: dependency versions and integration scenarios still need to be validated.&lt;/p&gt;

&lt;p&gt;The evolution also does not turn FluentMap into a full ORM. It does not introduce entity tracking, migration management, or generic CRUD generation in the core package.&lt;/p&gt;

&lt;p&gt;The goal remains to complement Dapper, not replace it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Property Mapping Is No Longer Enough
&lt;/h2&gt;

&lt;p&gt;Mapping a database column to a property solves many problems, but domain models are not always made up of simple scalar properties.&lt;/p&gt;

&lt;p&gt;In applications following Domain-Driven Design, for example, value objects, nested properties, and immutable entities are common. There are also situations where the same entity needs to be materialized from queries with different result shapes.&lt;/p&gt;

&lt;p&gt;FluentMap 3.x introduces specific mechanisms for these scenarios.&lt;/p&gt;

&lt;p&gt;An entity can now map a nested property:&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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerMap&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EntityMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;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;CustomerMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;City&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"city"&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;To materialize that path, we use one of FluentMap's dedicated query APIs:&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;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryMappedSingle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"SELECT 'Sao Paulo' AS city"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;Registering a nested mapping does not automatically make every standard Dapper query capable of constructing complex object structures. The functionality depends on using the appropriate materialization path.&lt;/p&gt;

&lt;p&gt;Beyond nested objects, the 3.x line introduces mapping profiles, property converters, and explicit construction strategies through &lt;code&gt;ConstructUsing(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mapping profiles allow an entity to support different query result shapes. Property converters handle transformations between database values and model properties. Explicit construction provides an alternative when automatic constructor selection does not meet an entity's requirements.&lt;/p&gt;

&lt;p&gt;These features expand the available modeling options, but they also introduce decisions that developers need to make deliberately.&lt;/p&gt;

&lt;p&gt;A Dapper &lt;code&gt;TypeHandler&amp;lt;T&amp;gt;&lt;/code&gt;, for example, remains a good choice when a type has a consistent scalar representation throughout an application. There is little reason to replace it with a property-specific converter if the existing mechanism already solves the problem correctly.&lt;/p&gt;

&lt;p&gt;This is a principle I try to preserve: a new capability does not automatically make an existing approach obsolete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global Configuration and Isolation: Two Execution Models
&lt;/h2&gt;

&lt;p&gt;Another important part of FluentMap's evolution concerns configuration.&lt;/p&gt;

&lt;p&gt;Historically, &lt;code&gt;FluentMapper.Initialize(...)&lt;/code&gt; established a global configuration. This model is straightforward and remains suitable for applications with a single set of mappings.&lt;/p&gt;

&lt;p&gt;However, global configuration requires attention in parallel tests, applications with multiple contexts, and scenarios where the same entity may need different mapping definitions.&lt;/p&gt;

&lt;p&gt;The 3.x line introduces immutable configuration and isolated &lt;code&gt;FluentMapRuntime&lt;/code&gt; instances:&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;runtime&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;FluentMapConfigurationBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerMap&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;Build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateRuntime&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;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryMappedSingle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sql&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 model, the configuration belongs to the runtime instance used to execute the query.&lt;/p&gt;

&lt;p&gt;A separate package, &lt;code&gt;FluentMap.DependencyInjection&lt;/code&gt;, also provides integration with .NET's dependency injection container.&lt;/p&gt;

&lt;p&gt;But there is an important boundary: isolation applies to materialization APIs controlled by FluentMap.&lt;/p&gt;

&lt;p&gt;Standard &lt;code&gt;Dapper.Query&amp;lt;T&amp;gt;()&lt;/code&gt; calls continue to use Dapper's globally registered type maps, while Dommel integration remains associated with global configuration.&lt;/p&gt;

&lt;p&gt;In other words, introducing dependency injection does not automatically eliminate global state from an application.&lt;/p&gt;

&lt;p&gt;Choosing between these models depends on the need for isolation. For a straightforward application, keeping the traditional configuration may result in lower maintenance overhead. For scenarios requiring independent mapping configurations, an isolated runtime provides greater control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving Some Work from Runtime to Compile Time
&lt;/h2&gt;

&lt;p&gt;One of the most interesting changes was introducing capabilities that operate during compilation.&lt;/p&gt;

&lt;p&gt;FluentMap now provides two additional packages: &lt;code&gt;FluentMap.Analyzers&lt;/code&gt; and &lt;code&gt;FluentMap.Generators&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Roslyn Analyzers identify certain configuration problems before the application runs. This helps detect mistakes that might otherwise surface only during initialization or query materialization.&lt;/p&gt;

&lt;p&gt;Source Generators provide generated mapping registration and materializers for supported scenarios.&lt;/p&gt;

&lt;p&gt;The architectural difference is that some work can move from runtime mechanisms into the compilation process, reducing dependence on reflection in those paths.&lt;/p&gt;

&lt;p&gt;This approach also made strict generated materialization possible. Instead of silently falling back to reflection-based materialization, strict mode rejects unsupported result shapes and configurations.&lt;/p&gt;

&lt;p&gt;There are potential benefits in predictability, diagnostics, and reducing reflection usage. However, it would be premature to claim that generated code makes every query faster.&lt;/p&gt;

&lt;p&gt;Performance depends on result shapes, data volumes, ADO.NET providers, and application characteristics. Improvements should be assessed through representative benchmarks, not assumed simply because code generation is involved.&lt;/p&gt;

&lt;p&gt;Similarly, although the project includes tests for specific Native AOT scenarios, version 3.6.0 does not claim full Native AOT compatibility. Reflection-based registration and other dynamic paths still have documented limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dommel and the Meaning of Persistence Operations
&lt;/h2&gt;

&lt;p&gt;The integration with Dommel has evolved as well.&lt;/p&gt;

&lt;p&gt;In the historical version, &lt;code&gt;Ignore()&lt;/code&gt; could end up being used as a workaround to prevent certain properties from being written to the database.&lt;/p&gt;

&lt;p&gt;However, excluding a property from mapping is not the same as declaring that it can be read but should not participate in an &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The current version distinguishes these cases through dedicated persistence metadata:&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="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DatabaseDefaultOnInsert&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Computed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is now possible to express whether a column has a database-generated default, is computed, or must be excluded from write operations.&lt;/p&gt;

&lt;p&gt;Version 3.5.0 also introduced &lt;code&gt;InsertMapped*&lt;/code&gt; and &lt;code&gt;UpdateMapped*&lt;/code&gt; operations, which can apply property converters during persistence.&lt;/p&gt;

&lt;p&gt;These operations are opt-in and do not automatically change the behavior of traditional Dommel calls.&lt;/p&gt;

&lt;p&gt;For applications migrating from earlier versions, this is an area that deserves particular attention. A query returning the expected data does not prove that inserts and updates still behave correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility Needs Evidence, Not Assumptions
&lt;/h2&gt;

&lt;p&gt;A recurring challenge with libraries built on ADO.NET is defining what compatibility with a database actually means.&lt;/p&gt;

&lt;p&gt;The fact that Dapper works with a particular provider does not automatically mean that every additional feature offered by another library has been validated against that provider.&lt;/p&gt;

&lt;p&gt;In version 3.6.0, FluentMap introduced documented compatibility levels supported by mandatory integration tests against real database instances.&lt;/p&gt;

&lt;p&gt;SQLite, SQL Server, PostgreSQL, MySQL, and MariaDB have certified coverage for both FluentMap Core and Dommel integration.&lt;/p&gt;

&lt;p&gt;Oracle and Firebird have Core certification, but not Dommel certification.&lt;/p&gt;

&lt;p&gt;SQL Server Compact Edition remains classified as a legacy integration without modern certification.&lt;/p&gt;

&lt;p&gt;This separation helps prevent an expectation of compatibility from becoming a guarantee that testing cannot support.&lt;/p&gt;

&lt;p&gt;The compatibility matrix also identifies the specific database and provider versions used during validation.&lt;/p&gt;

&lt;p&gt;The current packages declare support for Dapper versions starting at 2.1.79 and below 3.0.0. For Dommel, the supported range starts at 3.5.3 and remains below 4.0.0.&lt;/p&gt;

&lt;p&gt;Distributed packages continue to target &lt;code&gt;.NET Standard 2.0&lt;/code&gt;, preserving compatibility with a broad range of consuming applications.&lt;/p&gt;

&lt;p&gt;Beyond functional tests, project maintenance now includes public API compatibility checks, static analysis, code coverage, release artifacts, and automated compatibility validation.&lt;/p&gt;

&lt;p&gt;I don't consider these secondary activities.&lt;/p&gt;

&lt;p&gt;For an open-source library, the release process and the ability to detect regressions are part of the reliability we provide to consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Upgrade from Version 2.0?
&lt;/h2&gt;

&lt;p&gt;For applications still using FluentMap 2.0.0, my recommendation is to start with existing behavior rather than the new features.&lt;/p&gt;

&lt;p&gt;First, identify how mappings are registered, which Dapper and Dommel versions are installed, and whether any integrations depend on specific behaviors.&lt;/p&gt;

&lt;p&gt;Then update FluentMap packages to the same release version, build the application, validate mappings, and run the test suite.&lt;/p&gt;

&lt;p&gt;If Dommel is involved, pay particular attention to write operations, generated keys, database defaults, and ignored properties. Configuration combinations previously accepted may also produce validation errors in the newer version.&lt;/p&gt;

&lt;p&gt;Binary compatibility deserves attention too. The Dommel integration has differences in its public API, so full binary compatibility with the historical 2.0.0 package should not be assumed.&lt;/p&gt;

&lt;p&gt;Only after validating the existing application would I consider adopting features such as isolated runtimes, advanced materialization, or Source Generators.&lt;/p&gt;

&lt;p&gt;A successful migration does not require using every available feature.&lt;/p&gt;

&lt;p&gt;In many cases, the best decision is simply to upgrade the library while leaving most application code unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bringing FluentMap Back Means
&lt;/h2&gt;

&lt;p&gt;Between versions 2.0.0 and 3.6.0, FluentMap went from a library whose maintenance had effectively stopped to a project with active development, new capabilities, documentation, compatibility testing, and an established release process.&lt;/p&gt;

&lt;p&gt;But the most important aspect, in my view, is not the number of features added.&lt;/p&gt;

&lt;p&gt;It is the ability to keep using a familiar abstraction without forcing existing applications to change how they work, while giving new projects access to mechanisms better suited to their requirements.&lt;/p&gt;

&lt;p&gt;Maintaining open-source software means balancing two responsibilities that often pull in different directions: preserving what consumers already depend on and allowing the project to evolve.&lt;/p&gt;

&lt;p&gt;For FluentMap, the approach has been to preserve the historical API as the compatibility path and introduce most new behaviors as optional capabilities.&lt;/p&gt;

&lt;p&gt;There are still limitations, particularly around global integration isolation and Native AOT support. They are part of the current contract and should remain explicit.&lt;/p&gt;

&lt;p&gt;To me, this is one of the most important measures of a library's evolution: not simply offering more possibilities, but helping consumers understand its contracts, limitations, and the consequences of their choices.&lt;/p&gt;

&lt;p&gt;FluentMap is actively maintained again. And for developers working with Dapper, version 3.6.0 offers new options without requiring them to rewrite what already works.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/rodri-oliveira-dev/Dapper-FluentMap" rel="noopener noreferrer"&gt;Dapper FluentMap: GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rodri-oliveira-dev/Dapper-FluentMap/compare/v2.0.0...v3.6.0" rel="noopener noreferrer"&gt;Full Code Comparison: v2.0.0 to v3.6.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rodri-oliveira-dev/Dapper-FluentMap/releases/tag/v2.0.0" rel="noopener noreferrer"&gt;Version 2.0.0 Release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rodri-oliveira-dev/Dapper-FluentMap/releases/tag/v3.6.0" rel="noopener noreferrer"&gt;Version 3.6.0 Release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rodri-oliveira-dev/Dapper-FluentMap/blob/v3.6.0/MIGRATION.md" rel="noopener noreferrer"&gt;Migration Guide: 2.x to 3.x&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rodri-oliveira-dev/Dapper-FluentMap/blob/v3.6.0/COMPATIBILITY.md" rel="noopener noreferrer"&gt;Compatibility Matrix and Known Limitations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>dapper</category>
      <category>open</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Architecture Decisions Shouldn't Get Lost: Introducing ADR Guard for VS Code</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/architecture-decisions-shouldnt-get-lost-introducing-adr-guard-for-vs-code-3211</link>
      <guid>https://dev.to/rodri-oliveira-dev/architecture-decisions-shouldnt-get-lost-introducing-adr-guard-for-vs-code-3211</guid>
      <description>&lt;p&gt;There's a question almost every developer has asked while working on a mature codebase:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why was it built this way?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the answer is buried in an old GitHub discussion. Other times, it's hidden in a Slack thread, an outdated document, or, even worse, in the memory of someone who left the company two years ago.&lt;/p&gt;

&lt;p&gt;The code tells us &lt;em&gt;how&lt;/em&gt; something was implemented. It rarely tells us &lt;em&gt;why&lt;/em&gt; a particular approach was chosen, what alternatives were considered, or which constraints influenced the decision.&lt;/p&gt;

&lt;p&gt;And that's a problem.&lt;/p&gt;

&lt;p&gt;As software engineers, we spend considerable time reviewing code, writing tests, and maintaining documentation. Yet the reasoning behind some of our most important technical decisions often disappears over time.&lt;/p&gt;

&lt;p&gt;This is one of the problems I wanted to address when I started building &lt;a href="https://github.com/rodri-oliveira-dev/adr-guard" rel="noopener noreferrer"&gt;ADR Guard&lt;/a&gt;, an open-source tool focused on Architecture Decision Records (ADRs).&lt;/p&gt;

&lt;p&gt;More recently, I've been working on bringing that experience directly into Visual Studio Code.&lt;/p&gt;

&lt;p&gt;But before discussing the extension, it's worth looking at why architectural decisions deserve more attention in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Decisions Are Part of Your System's History
&lt;/h2&gt;

&lt;p&gt;An Architecture Decision Record is a document that captures an important architectural decision, including its context, the approach selected, and its consequences.&lt;/p&gt;

&lt;p&gt;The concept is straightforward.&lt;/p&gt;

&lt;p&gt;Imagine your team decides to move from synchronous communication between services to an event-driven architecture.&lt;/p&gt;

&lt;p&gt;At that moment, everyone probably understands why.&lt;/p&gt;

&lt;p&gt;Perhaps you need better resilience. Maybe your services have become too tightly coupled. Or perhaps you're dealing with scalability requirements that the current design cannot comfortably support.&lt;/p&gt;

&lt;p&gt;Now imagine someone revisiting that decision two years later.&lt;/p&gt;

&lt;p&gt;The system has evolved. The team has changed. New technologies are available, and the original constraints may no longer exist.&lt;/p&gt;

&lt;p&gt;Without an ADR, that developer needs to reconstruct the reasoning behind the decision.&lt;/p&gt;

&lt;p&gt;With an ADR, they have a starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An ADR isn't meant to prevent architectural decisions from changing. It's meant to make those changes informed and intentional.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Good architecture isn't about making decisions that last forever. It's about understanding the trade-offs we accept and knowing when it's time to revisit them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Challenge Isn't Writing ADRs
&lt;/h2&gt;

&lt;p&gt;I've noticed that adopting ADRs is often easier than maintaining the practice.&lt;/p&gt;

&lt;p&gt;Initially, everything looks promising.&lt;/p&gt;

&lt;p&gt;The team creates a &lt;code&gt;docs/adr&lt;/code&gt; directory, agrees on a template, and starts documenting important decisions alongside the source code.&lt;/p&gt;

&lt;p&gt;A few months later, inconsistencies begin to appear.&lt;/p&gt;

&lt;p&gt;Some documents are missing essential sections. Others use different formats. Certain decisions are never updated, even after significant architectural changes.&lt;/p&gt;

&lt;p&gt;Eventually, ADRs risk becoming another collection of Markdown files that nobody remembers to consult.&lt;/p&gt;

&lt;p&gt;I don't think the problem is necessarily a lack of discipline.&lt;/p&gt;

&lt;p&gt;The problem is that we sometimes introduce documentation practices without considering the developer experience around them.&lt;/p&gt;

&lt;p&gt;If creating, finding, editing, and validating architectural decisions requires too much effort, the practice becomes increasingly difficult to sustain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation needs to fit into the development workflow, not compete with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That idea became an important part of the ADR Guard project.&lt;/p&gt;

&lt;h2&gt;
  
  
  ADR Guard: Bringing Structure to Architectural Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rodri-oliveira-dev/adr-guard" rel="noopener noreferrer"&gt;ADR Guard&lt;/a&gt; is an open-source tool built with .NET to help teams manage Architecture Decision Records throughout their lifecycle.&lt;/p&gt;

&lt;p&gt;It doesn't attempt to replace Markdown or introduce a proprietary documentation platform.&lt;/p&gt;

&lt;p&gt;ADRs remain regular files in your repository, which means they can be versioned, reviewed through pull requests, and maintained alongside the code they describe.&lt;/p&gt;

&lt;p&gt;The CLI supports workflows such as creating records, validating their structure, organizing documentation, and performing incremental checks.&lt;/p&gt;

&lt;p&gt;It also supports different ADR formats, including canonical ADRs and MADR 4.0.&lt;/p&gt;

&lt;p&gt;The goal is to make architectural documentation more consistent and easier to maintain.&lt;/p&gt;

&lt;p&gt;For example, structural validation can identify documents that don't follow the expected format. Teams can also use baselines and automated checks to introduce governance incrementally instead of requiring an entire repository to become compliant overnight.&lt;/p&gt;

&lt;p&gt;These capabilities can be integrated into CI/CD workflows, making documentation quality part of the development process.&lt;/p&gt;

&lt;p&gt;There is, however, an important distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validating an ADR doesn't mean approving the architectural decision itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tool can verify structure, required sections, and documented conventions. It cannot replace engineering judgment, architectural discussions, or technical reviews.&lt;/p&gt;

&lt;p&gt;And it shouldn't try to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing ADRs Into Visual Studio Code
&lt;/h2&gt;

&lt;p&gt;While the CLI provides the foundation, I wanted to reduce the friction involved in everyday ADR management.&lt;/p&gt;

&lt;p&gt;Most developers already spend a significant portion of their day inside their editor.&lt;/p&gt;

&lt;p&gt;So why should working with architectural decisions require a completely separate experience?&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;ADR Guard for VS Code&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;The extension provides a visual interface for common ADR workflows while relying on the existing ADR Guard CLI for the underlying operations.&lt;/p&gt;

&lt;p&gt;This separation is intentional: the same core behavior can be used from the terminal, VS Code, or automation pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding Architectural Decisions Without Searching Through Folders
&lt;/h3&gt;

&lt;p&gt;One of the first improvements is discoverability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh9ib248q9wg2jq3bjgqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh9ib248q9wg2jq3bjgqo.png" alt="ADR Guard for VS Code showing the visual list of Architecture Decision Records" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The ADR Guard visual dashboard provides direct access to architectural decisions and their metadata.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of manually browsing Markdown files, developers can use a visual list displaying information such as ADR identifiers, titles, statuses, and file locations.&lt;/p&gt;

&lt;p&gt;The interface supports searching, filtering by status, sorting, and opening documents directly in the editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmb0e2gk2irfs0ah2nqxd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmb0e2gk2irfs0ah2nqxd.png" alt="ADR Guard search and status filtering in Visual Studio Code" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching and filtering ADRs makes architectural context easier to discover.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's also an ADR Explorer integrated into VS Code, providing another way to navigate records and recognized relationships.&lt;/p&gt;

&lt;p&gt;These features might sound simple, but I believe discoverability has a direct impact on whether documentation is actually used.&lt;/p&gt;

&lt;p&gt;A decision that's easy to find is much more likely to influence future engineering discussions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating ADRs Without Memorizing Templates
&lt;/h3&gt;

&lt;p&gt;Another area I wanted to improve was ADR creation.&lt;/p&gt;

&lt;p&gt;Markdown is a great format for technical documentation, but asking every developer to remember the structure of every supported template introduces unnecessary friction.&lt;/p&gt;

&lt;p&gt;The extension provides a visual creation form that helps guide the process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86z9ajxl2d2qjwf5cwx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86z9ajxl2d2qjwf5cwx4.png" alt="Creating an Architecture Decision Record using the ADR Guard Minimal template" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Minimal template focuses on the essential elements of an architectural decision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the Minimal template, developers can focus on three fundamental questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the context?&lt;/li&gt;
&lt;li&gt;What decision are we making?&lt;/li&gt;
&lt;li&gt;What are the consequences?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For decisions requiring more detailed analysis, the Extended template includes additional fields, such as decision drivers, alternatives considered, rationale, risks, and references.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdyrhwd5sr3xidstt7za.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdyrhwd5sr3xidstt7za.png" alt="ADR Guard Extended ADR template with additional architectural decision fields" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Extended template helps capture more detailed architectural reasoning and trade-offs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The extension also supports a Markdown preview before creating the document.&lt;/p&gt;

&lt;p&gt;The point isn't to automate architectural thinking. It's to remove repetitive tasks so developers can focus on explaining their decisions clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editing Existing Decisions Safely
&lt;/h3&gt;

&lt;p&gt;Architectural decisions evolve, and documentation needs to reflect that.&lt;/p&gt;

&lt;p&gt;The extension provides visual editing for compatible ADR documents, allowing developers to update their content without manually navigating every Markdown section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtodaqymg9kamz373h5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtodaqymg9kamz373h5w.png" alt="Editing an existing Architecture Decision Record in ADR Guard for VS Code" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Existing ADRs can be edited through a structured interface inside VS Code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One detail I consider particularly important is how the extension handles external modifications.&lt;/p&gt;

&lt;p&gt;If an ADR changes after being loaded into the editing form, the extension detects the conflict and prevents an outdated version from silently overwriting the newer content.&lt;/p&gt;

&lt;p&gt;This is especially relevant in collaborative repositories where multiple developers may be working on the same documentation.&lt;/p&gt;

&lt;p&gt;The goal is to provide convenience without sacrificing predictability or data integrity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making Validation Part of the Development Workflow
&lt;/h3&gt;

&lt;p&gt;Another useful capability is integration with VS Code's &lt;strong&gt;Problems&lt;/strong&gt; panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhjiwvgv0gshq5r4tlr89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhjiwvgv0gshq5r4tlr89.png" alt="ADR Guard validation diagnostics displayed in the VS Code Problems panel" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ADR validation findings appear alongside other development diagnostics in VS Code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When an ADR violates a supported structural rule, developers can see the corresponding diagnostic without leaving the editor.&lt;/p&gt;

&lt;p&gt;Validation can run explicitly, and automatic validation on save is available as an opt-in setting.&lt;/p&gt;

&lt;p&gt;The extension also provides workflows for validating changed ADRs against a local Git reference and checking findings against an existing baseline.&lt;/p&gt;

&lt;p&gt;I see this as a useful step toward making architectural documentation part of ordinary engineering feedback loops.&lt;/p&gt;

&lt;p&gt;We already expect immediate feedback when code violates certain rules. Having similar visibility for ADR structure can help teams maintain documentation standards without relying entirely on manual inspections.&lt;/p&gt;

&lt;p&gt;Of course, structural consistency isn't the same thing as architectural correctness. Human review remains essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Governance Shouldn't Become Another Source of Friction
&lt;/h2&gt;

&lt;p&gt;One concern I've had throughout this project is avoiding unnecessary complexity.&lt;/p&gt;

&lt;p&gt;It's easy to introduce tools and processes in the name of governance, only to make developers' lives harder.&lt;/p&gt;

&lt;p&gt;I believe architecture governance works best when its purpose is clear and its mechanisms are predictable.&lt;/p&gt;

&lt;p&gt;That's why the VS Code extension is designed as a user interface over the existing CLI rather than a separate implementation of ADR rules.&lt;/p&gt;

&lt;p&gt;It also avoids silently modifying architectural documents during ordinary navigation or validation.&lt;/p&gt;

&lt;p&gt;The developer remains responsible for deciding when records should be created or changed.&lt;/p&gt;

&lt;p&gt;To me, this is an important principle: &lt;strong&gt;good engineering tools should support deliberate decisions, not hide consequential actions behind convenience.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Every Technical Decision Needs an ADR
&lt;/h2&gt;

&lt;p&gt;I don't believe every implementation detail should become an architectural record.&lt;/p&gt;

&lt;p&gt;That would create unnecessary bureaucracy and eventually reduce the value of the documentation.&lt;/p&gt;

&lt;p&gt;ADRs are most useful for decisions that affect the system's structure, establish long-term technical constraints, introduce important dependencies, or involve meaningful trade-offs.&lt;/p&gt;

&lt;p&gt;Choosing between two local variable names probably doesn't need an ADR.&lt;/p&gt;

&lt;p&gt;Deciding how services communicate, how data ownership is defined, or why a particular persistence strategy was adopted probably deserves one.&lt;/p&gt;

&lt;p&gt;The objective isn't to produce more documentation.&lt;/p&gt;

&lt;p&gt;It's to preserve the context that future engineers will need to make good decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building ADR Guard has reinforced something I strongly believe about software architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The quality of an architecture depends not only on the decisions we make, but also on our ability to understand and revisit those decisions over time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An ADR won't solve architectural problems by itself.&lt;/p&gt;

&lt;p&gt;But it can preserve valuable engineering context, make trade-offs visible, and provide a foundation for better technical discussions.&lt;/p&gt;

&lt;p&gt;ADR Guard for VS Code is one more step toward making that practice easier to adopt and maintain.&lt;/p&gt;

&lt;p&gt;The project is open source and continues to evolve. If you work with software architecture, technical leadership, or long-lived systems, I'd be interested in hearing how your team handles architectural decisions.&lt;/p&gt;

&lt;p&gt;Do you use ADRs today? And if so, what's been the biggest challenge in keeping them useful?&lt;/p&gt;




&lt;h2&gt;
  
  
  Try ADR Guard
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=rodrioliveira.adr-guard" rel="noopener noreferrer"&gt;Install ADR Guard for VS Code&lt;/a&gt;&lt;/strong&gt; — Get the extension from the Visual Studio Marketplace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://rodri-oliveira-dev.github.io/adr-guard/product/vscode-visual-guide/" rel="noopener noreferrer"&gt;Official VS Code Visual Guide&lt;/a&gt;&lt;/strong&gt; — Explore the interface, screenshots, and supported workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/rodri-oliveira-dev/adr-guard" rel="noopener noreferrer"&gt;ADR Guard on GitHub&lt;/a&gt;&lt;/strong&gt; — Explore the source code, report issues, or contribute.&lt;/p&gt;

&lt;p&gt;The VS Code extension uses the ADR Guard CLI, which must be installed separately. Check the official documentation for installation, compatibility, and configuration details.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>vscode</category>
      <category>opensource</category>
      <category>adr</category>
    </item>
    <item>
      <title>GitHub Dotfiles: Treating Your Development Environment Like Code</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/github-dotfiles-treating-your-development-environment-like-code-2a04</link>
      <guid>https://dev.to/rodri-oliveira-dev/github-dotfiles-treating-your-development-environment-like-code-2a04</guid>
      <description>&lt;p&gt;Most developers eventually accumulate a collection of small configuration files, aliases, scripts, shell functions, and Git preferences that make their daily work a little more comfortable.&lt;/p&gt;

&lt;p&gt;At first, they usually live directly inside &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.gitconfig&lt;/code&gt;, random folders under &lt;code&gt;~/scripts&lt;/code&gt;, or maybe inside a note called something like &lt;em&gt;“things to install on a new machine”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That works surprisingly well — until you change computers.&lt;/p&gt;

&lt;p&gt;Or start using GitHub Codespaces.&lt;/p&gt;

&lt;p&gt;Or reinstall Linux.&lt;/p&gt;

&lt;p&gt;Or realize that the alias you depend on every day exists on one machine but not another.&lt;/p&gt;

&lt;p&gt;That is where &lt;strong&gt;dotfiles&lt;/strong&gt; become interesting.&lt;/p&gt;

&lt;p&gt;I maintain my development environment in a GitHub repository called &lt;a href="https://github.com/rodri-oliveira-dev/dotfiles" rel="noopener noreferrer"&gt;dotfiles&lt;/a&gt;. What started as a way to keep a few preferences synchronized gradually became something more useful: a reproducible, testable, and intentionally limited development environment for Git, Bash, .NET, and GitHub Codespaces.&lt;/p&gt;

&lt;p&gt;The important part isn't the aliases themselves.&lt;/p&gt;

&lt;p&gt;The interesting part is treating your personal development environment with some of the same engineering practices you would use in a production repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dotfiles are configuration as code
&lt;/h2&gt;

&lt;p&gt;On Unix-like systems, configuration files commonly start with a dot: &lt;code&gt;.bashrc&lt;/code&gt;, &lt;code&gt;.gitconfig&lt;/code&gt;, &lt;code&gt;.vimrc&lt;/code&gt;, and so on. Because they are hidden by default, they became known collectively as &lt;em&gt;dotfiles&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Putting them in Git gives you some immediate advantages.&lt;/p&gt;

&lt;p&gt;You get history. You can understand why a configuration changed.&lt;/p&gt;

&lt;p&gt;You get portability. A new environment can start from a known baseline instead of being rebuilt from memory.&lt;/p&gt;

&lt;p&gt;And you get rollback. When an experiment with your shell configuration goes badly, &lt;code&gt;git diff&lt;/code&gt; is a much nicer debugging tool than trying to remember what the file looked like yesterday.&lt;/p&gt;

&lt;p&gt;But versioning dotfiles is only the first step.&lt;/p&gt;

&lt;p&gt;The bigger question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What actually belongs in a personal environment repository?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That boundary turned out to be one of the most important design decisions in my setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your dotfiles should not own your projects
&lt;/h2&gt;

&lt;p&gt;My repository deliberately manages &lt;strong&gt;developer-level preferences&lt;/strong&gt;, not project dependencies.&lt;/p&gt;

&lt;p&gt;For example, Git behavior and shell aliases belong to the dotfiles repository. A project's .NET SDK version does not.&lt;/p&gt;

&lt;p&gt;If a repository requires a particular .NET SDK, that decision belongs in its &lt;code&gt;global.json&lt;/code&gt;. NuGet package versions belong in project files or &lt;code&gt;Directory.Packages.props&lt;/code&gt;. Local .NET tools belong in &lt;code&gt;.config/dotnet-tools.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same principle applies to the editor. A project may recommend VS Code extensions through &lt;code&gt;.vscode/extensions.json&lt;/code&gt;, while personal editor preferences can remain in VS Code Settings Sync.&lt;/p&gt;

&lt;p&gt;This separation prevents a surprisingly common problem: your personal workstation configuration slowly becoming an undocumented dependency of the applications you build.&lt;/p&gt;

&lt;p&gt;A project should still describe how it wants to be built when another developer clones it.&lt;/p&gt;

&lt;p&gt;Your dotfiles should make &lt;strong&gt;you&lt;/strong&gt; more productive while respecting that contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe installer is more important than a clever installer
&lt;/h2&gt;

&lt;p&gt;The entry point of the repository is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/rodri-oliveira-dev/dotfiles.git &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/dotfiles"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/dotfiles"&lt;/span&gt;
./install.sh
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.bashrc"&lt;/span&gt;
dotfiles-doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting work happens behind &lt;code&gt;install.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of replacing the entire &lt;code&gt;~/.bashrc&lt;/code&gt;, the installer adds a clearly delimited managed block. That block loads the repository's shell fragments and makes &lt;code&gt;~/.local/bin&lt;/code&gt; available through &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The actual managed configuration lives under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${XDG_CONFIG_HOME:-$HOME/.config}/rodri-dotfiles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer creates links from that stable location to the files stored in the cloned repository.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A naïve dotfiles installer might simply copy a new &lt;code&gt;.bashrc&lt;/code&gt; over the user's existing one. That is easy to implement, but fairly hostile to environments such as Codespaces, Linux distributions, corporate machines, or anything else that has already added useful shell initialization.&lt;/p&gt;

&lt;p&gt;This repository instead tries to own only the portion it created.&lt;/p&gt;

&lt;p&gt;If an existing destination is not one of the symlinks the repository expects to manage, installation stops rather than replacing it.&lt;/p&gt;

&lt;p&gt;That behavior is slightly less convenient than &lt;code&gt;cp -f&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is also much harder to regret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency changes everything
&lt;/h2&gt;

&lt;p&gt;A setup script becomes considerably more useful when running it twice produces the same result as running it once.&lt;/p&gt;

&lt;p&gt;That is the goal here.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;install.sh&lt;/code&gt; repeatedly should not keep appending blocks to &lt;code&gt;.bashrc&lt;/code&gt;, duplicating Git configuration, or creating additional links.&lt;/p&gt;

&lt;p&gt;This is especially important with GitHub Codespaces because environment creation becomes an automated lifecycle operation rather than something you manually perform once every few years.&lt;/p&gt;

&lt;p&gt;The repository can be configured as a GitHub Codespaces dotfiles source, allowing new Codespaces to bootstrap the personal configuration automatically.&lt;/p&gt;

&lt;p&gt;At that point, idempotency is no longer just nice engineering hygiene.&lt;/p&gt;

&lt;p&gt;It becomes part of the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git configuration without replacing &lt;code&gt;.gitconfig&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Git gets the same treatment.&lt;/p&gt;

&lt;p&gt;Rather than owning the user's complete global configuration, the installer adds an &lt;code&gt;include.path&lt;/code&gt; pointing at the managed Git configuration.&lt;/p&gt;

&lt;p&gt;The repository currently defines defaults such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[init]&lt;/span&gt;
    &lt;span class="py"&gt;defaultBranch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="nn"&gt;[fetch]&lt;/span&gt;
    &lt;span class="py"&gt;prune&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[push]&lt;/span&gt;
    &lt;span class="py"&gt;autoSetupRemote&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[rerere]&lt;/span&gt;
    &lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[diff]&lt;/span&gt;
    &lt;span class="py"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;histogram&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are personal defaults, not project policies.&lt;/p&gt;

&lt;p&gt;Your employer's repository can still have its own configuration. Another tool can still add global Git settings. The dotfiles configuration participates in the final Git configuration instead of trying to become the entire configuration.&lt;/p&gt;

&lt;p&gt;That composability is something I now consider essential in a dotfiles repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small aliases are useful. Context-aware helpers are better.
&lt;/h2&gt;

&lt;p&gt;Of course, the repository still contains the usual conveniences.&lt;/p&gt;

&lt;p&gt;For Git, I use aliases such as &lt;code&gt;gs&lt;/code&gt;, &lt;code&gt;gd&lt;/code&gt;, and &lt;code&gt;gl&lt;/code&gt;. For .NET, there are shortcuts such as &lt;code&gt;dr&lt;/code&gt; for restore, &lt;code&gt;db&lt;/code&gt; for build, and &lt;code&gt;dt&lt;/code&gt; for tests.&lt;/p&gt;

&lt;p&gt;Those save keystrokes.&lt;/p&gt;

&lt;p&gt;The more interesting commands are the repository-aware helpers.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet-context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can inspect the current Git repository and report conventions such as &lt;code&gt;global.json&lt;/code&gt;, &lt;code&gt;Directory.Build.props&lt;/code&gt;, &lt;code&gt;Directory.Packages.props&lt;/code&gt;, local tool manifests, and solution files.&lt;/p&gt;

&lt;p&gt;Another command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet-repo-doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;provides a broader diagnostic view of a .NET repository, including SDK resolution, solutions, projects, test projects, target frameworks, Central Package Management, and other common configuration.&lt;/p&gt;

&lt;p&gt;There is also:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet-deps vulnerable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for dependency diagnostics, and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet-why Microsoft.CodeAnalysis.Common
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for answering one of those wonderfully annoying questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why is this package even in my dependency graph?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And before pushing a change, I can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet-verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which coordinates restore, build, formatting verification, and tests using the repository's own configuration.&lt;/p&gt;

&lt;p&gt;The goal is not to hide the .NET CLI.&lt;/p&gt;

&lt;p&gt;It is to encode the repetitive discovery and safety checks that normally happen before invoking it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respecting &lt;code&gt;global.json&lt;/code&gt; is a small but important detail
&lt;/h2&gt;

&lt;p&gt;One temptation when creating a developer bootstrap repository is installing your favorite SDK and tool versions globally.&lt;/p&gt;

&lt;p&gt;I intentionally avoided that.&lt;/p&gt;

&lt;p&gt;Different projects may target different SDK feature bands. Your dotfiles should not silently decide that every repository on your machine needs the same one.&lt;/p&gt;

&lt;p&gt;The helpers therefore resolve the SDK from the repository context instead of trying to impose one globally.&lt;/p&gt;

&lt;p&gt;The same idea applies to .NET tools.&lt;/p&gt;

&lt;p&gt;When a tool is part of a project's build process, a local tool manifest is a much better source of truth than someone's workstation.&lt;/p&gt;

&lt;p&gt;This is a subtle architectural boundary, but it dramatically improves reproducibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostics deserve a first-class command
&lt;/h2&gt;

&lt;p&gt;Automated setup is great until something becomes slightly wrong.&lt;/p&gt;

&lt;p&gt;That is why the repository includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotfiles-doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The doctor checks the assumptions that make the environment work: shell integration, managed symlinks, &lt;code&gt;PATH&lt;/code&gt;, Git includes, repository hooks, helper commands, Bash, Git, and the available .NET SDK.&lt;/p&gt;

&lt;p&gt;This is one of my favorite patterns in developer tooling.&lt;/p&gt;

&lt;p&gt;Installation says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is how the environment should look.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A doctor command says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is whether it still looks that way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference becomes increasingly valuable as the environment evolves.&lt;/p&gt;

&lt;p&gt;Instead of telling someone to inspect four configuration files and three symlinks manually, you give the environment an executable health check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates are a trust boundary
&lt;/h2&gt;

&lt;p&gt;Updating dotfiles sounds trivial.&lt;/p&gt;

&lt;p&gt;You could simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then execute the installer again.&lt;/p&gt;

&lt;p&gt;But there is a security detail hiding inside that workflow: &lt;strong&gt;the repository contains executable shell scripts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Updating the repository therefore changes code that you may execute automatically.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dotfiles-update&lt;/code&gt; helper treats that as a trust boundary.&lt;/p&gt;

&lt;p&gt;It requires a clean worktree, verifies the expected &lt;code&gt;main&lt;/code&gt; branch and &lt;code&gt;origin/main&lt;/code&gt; upstream, validates that &lt;code&gt;origin&lt;/code&gt; points to the expected repository, rejects unexpected URL rewriting, fetches the update, requires a fast-forward, verifies expected executable files, and only then refreshes the installation.&lt;/p&gt;

&lt;p&gt;Afterward, it runs &lt;code&gt;dotfiles-doctor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This does not magically make remote code trustworthy. That would be an unrealistic claim.&lt;/p&gt;

&lt;p&gt;It does make the transition from one trusted local revision to another much more explicit.&lt;/p&gt;

&lt;p&gt;That is a useful distinction whenever an updater ends by executing newly downloaded code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uninstalling cleanly is part of installation design
&lt;/h2&gt;

&lt;p&gt;There is another feature that configuration scripts often forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./uninstall.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uninstaller removes what the repository owns and leaves unrelated configuration alone.&lt;/p&gt;

&lt;p&gt;That requirement influences the entire architecture.&lt;/p&gt;

&lt;p&gt;If you cannot identify which lines, files, symlinks, and Git configuration entries belong to your installer, safely removing them later becomes difficult.&lt;/p&gt;

&lt;p&gt;In other words, a good uninstall story is usually evidence of a good ownership model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dotfiles can leak more than you expect
&lt;/h2&gt;

&lt;p&gt;There is one rule worth repeating whenever dotfiles and GitHub appear in the same sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not commit secrets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shell configuration is exactly the kind of place where credentials tend to accumulate over time.&lt;/p&gt;

&lt;p&gt;A quick &lt;code&gt;export API_KEY=...&lt;/code&gt; can become permanent history surprisingly easily.&lt;/p&gt;

&lt;p&gt;The repository therefore treats secret scanning as part of CI rather than relying purely on memory.&lt;/p&gt;

&lt;p&gt;Its validation pipeline currently includes Gitleaks for secret detection, actionlint for GitHub Actions validation, zizmor for workflow security auditing, and Hadolint for the lifecycle test Dockerfile.&lt;/p&gt;

&lt;p&gt;Shell scripts are additionally checked using Bash syntax validation, ShellCheck, and shfmt.&lt;/p&gt;

&lt;p&gt;The important lesson is not that every dotfiles repository needs exactly those tools.&lt;/p&gt;

&lt;p&gt;The lesson is that configuration repositories can execute code and can contain credentials, which means they deserve a security model too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yes, I test my dotfiles
&lt;/h2&gt;

&lt;p&gt;This may sound excessive until you consider what an installer does.&lt;/p&gt;

&lt;p&gt;It modifies shell startup behavior, Git configuration, symlinks, executable paths, and potentially every development environment you create.&lt;/p&gt;

&lt;p&gt;That is worth testing.&lt;/p&gt;

&lt;p&gt;The repository uses Bats to verify behavior such as installer idempotency, lifecycle operations, repository discovery, updater behavior, and the .NET helpers.&lt;/p&gt;

&lt;p&gt;It also builds a clean Ubuntu test container.&lt;/p&gt;

&lt;p&gt;That container verifies the lifecycle from a fresh environment: installation, diagnostics, idempotency, repository hooks, and uninstall.&lt;/p&gt;

&lt;p&gt;There is even a deny-by-default Docker context so unrelated local files are not accidentally copied into the lifecycle image.&lt;/p&gt;

&lt;p&gt;At some point, dotfiles stop being “some configuration files”.&lt;/p&gt;

&lt;p&gt;They become a small software product whose user happens to be you.&lt;/p&gt;

&lt;p&gt;And once you see them that way, tests stop looking strange.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would recommend starting with
&lt;/h2&gt;

&lt;p&gt;You absolutely do not need all of this on day one.&lt;/p&gt;

&lt;p&gt;Start with the configuration that causes friction when you move between machines.&lt;/p&gt;

&lt;p&gt;Put your Git preferences and shell aliases in a repository. Add a simple bootstrap script. Make it safe to execute twice.&lt;/p&gt;

&lt;p&gt;Then resist the temptation to turn the repository into a universal package manager for your entire workstation.&lt;/p&gt;

&lt;p&gt;Add automation only when you repeatedly encounter a problem worth automating.&lt;/p&gt;

&lt;p&gt;The repository should grow with your workflow, not become another environment you have to maintain for its own sake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick takeaways
&lt;/h2&gt;

&lt;p&gt;If you remember only a few things from this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat dotfiles as &lt;strong&gt;personal configuration as code&lt;/strong&gt;, not as a replacement for project configuration.&lt;/li&gt;
&lt;li&gt;Make installation &lt;strong&gt;idempotent and conservative&lt;/strong&gt;; avoid overwriting configuration you do not own.&lt;/li&gt;
&lt;li&gt;Keep SDKs, packages, tools, and other project dependencies &lt;strong&gt;inside the project that needs them&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give automated environments a &lt;strong&gt;doctor command&lt;/strong&gt; so assumptions can be verified later.&lt;/li&gt;
&lt;li&gt;Consider updates to executable dotfiles a &lt;strong&gt;trust boundary&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Never store credentials in a public dotfiles repository, and consider automated secret scanning.&lt;/li&gt;
&lt;li&gt;Once your installer starts changing important parts of your environment, &lt;strong&gt;test it like software&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;If you want to explore the implementation discussed here, the complete repository is available at &lt;strong&gt;rodri-oliveira-dev/dotfiles&lt;/strong&gt; on GitHub. The most interesting places to start are &lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;install.sh&lt;/code&gt;, &lt;code&gt;bin/dotfiles-doctor&lt;/code&gt;, &lt;code&gt;bin/dotfiles-update&lt;/code&gt;, &lt;code&gt;shell/&lt;/code&gt;, &lt;code&gt;git/config&lt;/code&gt;, &lt;code&gt;.github/workflows/validate.yml&lt;/code&gt;, and &lt;code&gt;tests/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the broader concepts, GitHub's documentation about personalizing Codespaces with dotfiles is an excellent next step. The Git documentation for configuration includes is useful for understanding the &lt;code&gt;include.path&lt;/code&gt; approach used here, while ShellCheck is worth studying if you maintain non-trivial Bash scripts.&lt;/p&gt;

&lt;p&gt;Bats is also a surprisingly useful tool to learn if you want executable tests around shell behavior. And if your dotfiles are stored remotely — especially in a public repository — studying secret scanning tools such as Gitleaks is time well spent.&lt;/p&gt;

&lt;p&gt;The main idea, however, is simpler than any particular tool.&lt;/p&gt;

&lt;p&gt;A development environment is part of the way we build software. Once we make that environment reproducible, observable, and intentionally scoped, moving to a new machine stops being a rebuilding exercise and becomes something much closer to deploying a known configuration.&lt;/p&gt;

&lt;p&gt;And that is a much nicer place to be.&lt;/p&gt;

</description>
      <category>dotfiles</category>
      <category>github</category>
      <category>dotnet</category>
      <category>devtools</category>
    </item>
    <item>
      <title>What Happens After the Commit?</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:01:00 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/what-happens-after-the-commit-1a4c</link>
      <guid>https://dev.to/rodri-oliveira-dev/what-happens-after-the-commit-1a4c</guid>
      <description>&lt;p&gt;Saving a record to a database is relatively easy.&lt;/p&gt;

&lt;p&gt;We have mature tools for that problem. We open a transaction, change some state, commit it, and roll everything back if something fails before the commit.&lt;/p&gt;

&lt;p&gt;Things become much more interesting when the database commit is &lt;strong&gt;not the end of the operation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine a financial transaction. Once it is persisted, another service needs to know about it so it can update a balance projection. Maybe additional consumers will react to the same event later.&lt;/p&gt;

&lt;p&gt;The first implementation that comes to mind seems perfectly reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;save to database
publish event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But there is an uncomfortable question between those two lines:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the database commits successfully and the message broker becomes unavailable immediately afterward?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The transaction exists.&lt;/p&gt;

&lt;p&gt;The event does not.&lt;/p&gt;

&lt;p&gt;And now two parts of the system are telling different stories.&lt;/p&gt;

&lt;p&gt;This is one of the problems I wanted to explore in &lt;code&gt;poc-arquitetura&lt;/code&gt;, a repository I use as an executable software architecture laboratory.&lt;/p&gt;

&lt;p&gt;The project uses .NET, PostgreSQL, Kafka, Keycloak, OpenTelemetry, k6, and a few other tools. But the goal has never been to collect technologies or architectural patterns.&lt;/p&gt;

&lt;p&gt;What interests me is what happens when these patterns have to &lt;strong&gt;work together&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Outbox solves one problem and introduces new responsibilities. At-least-once delivery forces us to think about idempotency. Eventual consistency changes how we reason about reads. A Saga requires compensation. Retry needs boundaries. Messaging requires observability beyond HTTP.&lt;/p&gt;

&lt;p&gt;That simple question — &lt;em&gt;what happens after the commit?&lt;/em&gt; — opens a much broader discussion about distributed systems.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rodri-oliveira-dev" rel="noopener noreferrer"&gt;
        rodri-oliveira-dev
      &lt;/a&gt; / &lt;a href="https://github.com/rodri-oliveira-dev/poc-arquitetura" rel="noopener noreferrer"&gt;
        poc-arquitetura
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      .NET architecture PoC for distributed services with CQRS, Kafka, PostgreSQL, Outbox, DLQ, observability and CI quality gates.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;poc-arquitetura&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/rodri-oliveira-dev/poc-arquitetura/actions/workflows/dotnet.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fe3131f0ae74307a8b6fee0b1aef474c4350a03abbe765f7bc5c23de95e7b9d5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f6472692d6f6c6976656972612d6465762f706f632d61727175697465747572612f646f746e65742e796d6c3f6272616e63683d6d61696e266c6162656c3d6275696c64" alt="Build"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rodri-oliveira-dev/poc-arquitetura/actions/workflows/dotnet.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/243e7d62ebda78d9a12d30c0d637d34ba406b27d4ae140b0dc40ec6c02c2365a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f6472692d6f6c6976656972612d6465762f706f632d61727175697465747572612f646f746e65742e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473" alt="Tests"&gt;&lt;/a&gt;
&lt;a href="https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_poc-arquitetura" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1cbca7d4eb2ec66bbc0533942842415c0e083cffef775ff5bddff0b05392e71f/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d726f6472692d6f6c6976656972612d6465765f706f632d6172717569746574757261266d65747269633d616c6572745f737461747573" alt="Quality Gate Status"&gt;&lt;/a&gt;
&lt;a href="https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_poc-arquitetura" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/efb4081b14125ef862be0bab0ceb1c09bfd3cf406d3f06487b8eb0ee6391ac36/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d726f6472692d6f6c6976656972612d6465765f706f632d6172717569746574757261266d65747269633d73656375726974795f726174696e67" alt="Security Rating"&gt;&lt;/a&gt;
&lt;a href="https://rodri-oliveira-dev.github.io/poc-arquitetura/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e03cfc430fac7d97d80b41b3e219c9467a412ba45611163352c3e30055b39c09/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f6472692d6f6c6976656972612d6465762f706f632d61727175697465747572612f70616765732d6172636869746563747572652e796d6c3f6272616e63683d6d61696e266c6162656c3d617263686974656374757265253230646f6373" alt="Architecture Docs"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;POC educacional de microserviços em .NET para estudar arquitetura de software com código real: Clean Architecture, DDD, PostgreSQL, Kafka, Outbox, Inbox, JWT/JWKS com Keycloak, observabilidade, segurança, contratos e testes automatizados.&lt;/p&gt;
&lt;p&gt;Ela demonstra um problema comum em sistemas financeiros: registrar fatos de forma transacional, publicar eventos com confiabilidade, projetar saldos em outro serviço e operar falhas sem esconder consistência eventual. O repositório também mostra contextos de identidade, transferência, pagamento externo e auditoria funcional para exercitar trade-offs de integração.&lt;/p&gt;
&lt;p&gt;Este projeto é útil para:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;quem está aprendendo arquitetura e quer ver os conceitos aplicados;&lt;/li&gt;
&lt;li&gt;desenvolvedores .NET que querem executar, testar e alterar uma stack local;&lt;/li&gt;
&lt;li&gt;arquitetos que querem avaliar decisões, limites e riscos;&lt;/li&gt;
&lt;li&gt;avaliadores técnicos que querem entender a proposta rapidamente.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Visão geral&lt;/h2&gt;
&lt;/div&gt;

  &lt;div class="js-render-enrichment-target"&gt;
    &lt;div class="render-plaintext-hidden"&gt;
      &lt;pre&gt;flowchart LR
    Client[Cliente ou teste] --&amp;gt; Keycloak[Keycloak OIDC]
    Client --&amp;gt; LedgerApi[LedgerService.Api]
    Client --&amp;gt; BalanceApi[BalanceService.Api]
    Client --&amp;gt; TransferApi[TransferService.Api]
    Client --&amp;gt; PaymentApi[PaymentService.Api]
    Client --&amp;gt; IdentityApi[IdentityService.Api]
    Client --&amp;gt; AuditApi[AuditService.Api]
    LedgerApi --&amp;gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rodri-oliveira-dev/poc-arquitetura" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  Start by separating facts from projections
&lt;/h2&gt;

&lt;p&gt;Two bounded contexts are especially important in this example.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LedgerService&lt;/code&gt; owns financial facts.&lt;/p&gt;

&lt;p&gt;If a financial transaction happened, the Ledger is where that fact should be recorded.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BalanceService&lt;/code&gt; has a different responsibility. It maintains a projection optimized for balance queries.&lt;/p&gt;

&lt;p&gt;At a high level, the architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Client[Client] --&amp;gt; LedgerApi[Ledger API]

    LedgerApi --&amp;gt; LedgerDb[(PostgreSQL&amp;lt;br/&amp;gt;Ledger)]

    LedgerDb --&amp;gt; LedgerWorker[Ledger Worker]
    LedgerWorker --&amp;gt; Kafka[(Kafka)]

    Kafka --&amp;gt; BalanceWorker[Balance Worker]
    BalanceWorker --&amp;gt; BalanceDb[(PostgreSQL&amp;lt;br/&amp;gt;Balance)]

    Client --&amp;gt; BalanceApi[Balance API]
    BalanceApi --&amp;gt; BalanceDb&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;A transaction and a balance are not the same thing.&lt;/p&gt;

&lt;p&gt;The transaction is a fact: something happened.&lt;/p&gt;

&lt;p&gt;The balance is a model derived from those facts.&lt;/p&gt;

&lt;p&gt;Could everything live in a single application and database? Absolutely. Depending on the system, that may even be the better architecture.&lt;/p&gt;

&lt;p&gt;But this laboratory deliberately separates those responsibilities so that the consequences of the decision become visible.&lt;/p&gt;

&lt;p&gt;One consequence appears immediately.&lt;/p&gt;

&lt;p&gt;The balance is no longer updated inside the same database transaction as the Ledger.&lt;/p&gt;

&lt;p&gt;We have entered the world of &lt;strong&gt;eventual consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That does not mean the system is simply inconsistent or incorrect. It means there is a period during which the Ledger already knows about a transaction while the Balance projection has not processed it yet.&lt;/p&gt;

&lt;p&gt;That window is not an accident.&lt;/p&gt;

&lt;p&gt;It is part of the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem is not Kafka. It is the dual write
&lt;/h2&gt;

&lt;p&gt;Suppose the Ledger does something like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant L as Ledger API
    participant DB as PostgreSQL
    participant K as Kafka

    C-&amp;gt;&amp;gt;L: Create transaction
    L-&amp;gt;&amp;gt;DB: INSERT LedgerEntry
    DB--&amp;gt;&amp;gt;L: COMMIT
    L-&amp;gt;&amp;gt;K: Publish event
    K--&amp;gt;&amp;gt;L: Acknowledged
    L--&amp;gt;&amp;gt;C: 201 Created

    Note over L,K: What happens if the process&amp;lt;br/&amp;gt;fails after COMMIT but before publish?&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When everything works, this looks fine.&lt;/p&gt;

&lt;p&gt;Now imagine that the database commit succeeds but the process crashes before Kafka acknowledges the event.&lt;/p&gt;

&lt;p&gt;The Ledger contains the transaction.&lt;/p&gt;

&lt;p&gt;The event may never be published.&lt;/p&gt;

&lt;p&gt;Publishing first and saving afterward does not solve the problem either. It simply reverses it: we can now publish an event for a transaction that later fails to commit.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;dual-write problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We need to update two independent resources — the database and the broker — and would like them to behave as if they belonged to one atomic transaction.&lt;/p&gt;

&lt;p&gt;One option would be some form of distributed transaction.&lt;/p&gt;

&lt;p&gt;For this project, I chose a different approach: accept that PostgreSQL and Kafka have independent lifecycles and make the &lt;strong&gt;intent to publish durable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is where the Transactional Outbox pattern becomes useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Outbox: do not publish now, persist the intent to publish
&lt;/h2&gt;

&lt;p&gt;Instead of saving a transaction and immediately depending on Kafka, the Ledger stores both the financial fact and an Outbox message inside the &lt;strong&gt;same PostgreSQL transaction&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as Client
    participant L as Ledger API
    participant DB as PostgreSQL
    participant W as Ledger Worker
    participant K as Kafka

    C-&amp;gt;&amp;gt;L: Create transaction

    rect rgb(235, 235, 235)
        L-&amp;gt;&amp;gt;DB: BEGIN
        L-&amp;gt;&amp;gt;DB: INSERT LedgerEntry
        L-&amp;gt;&amp;gt;DB: INSERT OutboxMessage
        L-&amp;gt;&amp;gt;DB: COMMIT
    end

    L--&amp;gt;&amp;gt;C: Transaction confirmed

    W-&amp;gt;&amp;gt;DB: Fetch pending messages
    DB--&amp;gt;&amp;gt;W: OutboxMessage
    W-&amp;gt;&amp;gt;K: Publish LedgerEntryCreated
    K--&amp;gt;&amp;gt;W: Acknowledged
    W-&amp;gt;&amp;gt;DB: Mark as processed&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Either both records are persisted or neither is.&lt;/p&gt;

&lt;p&gt;The HTTP request no longer needs Kafka to be available in order to preserve the intent to publish the event.&lt;/p&gt;

&lt;p&gt;A separate worker polls pending Outbox messages and publishes them.&lt;/p&gt;

&lt;p&gt;After Kafka confirms publication, the worker updates the Outbox entry.&lt;/p&gt;

&lt;p&gt;If Kafka is unavailable, the financial transaction still exists &lt;strong&gt;together with durable information that an event still needs to be published&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the worker crashes, processing can resume later.&lt;/p&gt;

&lt;p&gt;This small architectural change has an important effect.&lt;/p&gt;

&lt;p&gt;The failure is no longer a tiny invisible window between two independent writes.&lt;/p&gt;

&lt;p&gt;It becomes &lt;strong&gt;state that the system can observe and recover from&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That, to me, is one of the most useful ways to understand Outbox.&lt;/p&gt;

&lt;p&gt;It does not magically turn Kafka and PostgreSQL into a single transaction.&lt;/p&gt;

&lt;p&gt;It does not guarantee exactly-once processing.&lt;/p&gt;

&lt;p&gt;It turns an otherwise difficult-to-recover failure into something explicitly represented in the system.&lt;/p&gt;

&lt;p&gt;Recoverable systems are usually far more interesting than systems designed around the assumption that failures will not happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Preventing message loss creates another problem: duplicates
&lt;/h2&gt;

&lt;p&gt;There is a consequence to this design.&lt;/p&gt;

&lt;p&gt;Imagine the worker publishes an event successfully, but crashes before marking the Outbox message as processed.&lt;/p&gt;

&lt;p&gt;When it restarts, the same message may be published again.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant O as Outbox
    participant W as Ledger Worker
    participant K as Kafka
    participant B as Balance Worker
    participant DB as Balance DB

    O-&amp;gt;&amp;gt;W: Pending message
    W-&amp;gt;&amp;gt;K: Publish event
    K--&amp;gt;&amp;gt;W: Success

    Note over W: Worker crashes before&amp;lt;br/&amp;gt;marking the message as processed

    O-&amp;gt;&amp;gt;W: Same message again
    W-&amp;gt;&amp;gt;K: Republish event

    K-&amp;gt;&amp;gt;B: Event
    B-&amp;gt;&amp;gt;DB: Was this event_id processed?
    DB--&amp;gt;&amp;gt;B: No
    B-&amp;gt;&amp;gt;DB: Update projection and store event_id

    K-&amp;gt;&amp;gt;B: Duplicate event
    B-&amp;gt;&amp;gt;DB: Was this event_id processed?
    DB--&amp;gt;&amp;gt;B: Yes

    Note over B: Duplicate safely ignored&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This is not necessarily a bug.&lt;/p&gt;

&lt;p&gt;It is a normal consequence of &lt;strong&gt;at-least-once delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The responsibility now moves to the consumer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BalanceService&lt;/code&gt; records processed event identifiers so the same event does not modify the projection twice.&lt;/p&gt;

&lt;p&gt;That is idempotency becoming part of the architecture.&lt;/p&gt;

&lt;p&gt;Retries are often presented as generic resilience configuration: retry three times, use exponential backoff, done.&lt;/p&gt;

&lt;p&gt;But retry is also a business decision.&lt;/p&gt;

&lt;p&gt;If executing the same operation twice produces two different side effects, a retry may cause more damage than the original failure.&lt;/p&gt;

&lt;p&gt;Before asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How many times should we retry?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think a better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens if we execute this operation again?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question tends to uncover much more important design problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Eventual consistency has to exist outside the diagram too
&lt;/h2&gt;

&lt;p&gt;Once Ledger and Balance are separated, this state becomes perfectly valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ledger:
Transaction exists.

Balance:
Transaction has not been projected yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is nothing inherently wrong with that.&lt;/p&gt;

&lt;p&gt;The problem appears when the architecture is asynchronous but the rest of the product behaves as though every read must immediately reflect every write.&lt;/p&gt;

&lt;p&gt;Different domains solve this in different ways.&lt;/p&gt;

&lt;p&gt;Some operations can expose a processing state. Some critical queries may need to consult the source of truth. Some applications can simply tolerate a small delay before projections become visible.&lt;/p&gt;

&lt;p&gt;There is no universal answer.&lt;/p&gt;

&lt;p&gt;The important point is that eventual consistency should be a &lt;strong&gt;known business and architectural property&lt;/strong&gt;, not an accidental side effect of adding Kafka.&lt;/p&gt;


&lt;h2&gt;
  
  
  Events are APIs too
&lt;/h2&gt;

&lt;p&gt;Once services depend on events, another problem eventually appears:&lt;/p&gt;

&lt;p&gt;contracts change.&lt;/p&gt;

&lt;p&gt;The project includes an evolution from events such as:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LedgerEntryCreated.v1
LedgerEntryCreated.v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;During a migration period, consumers may need to understand both versions.&lt;/p&gt;

&lt;p&gt;That forces us to treat event schemas as real integration contracts.&lt;/p&gt;

&lt;p&gt;We already think carefully about HTTP APIs: OpenAPI definitions, breaking changes, versioning, client compatibility.&lt;/p&gt;

&lt;p&gt;Events deserve similar care, perhaps even more.&lt;/p&gt;

&lt;p&gt;An HTTP response usually exists only during a request.&lt;/p&gt;

&lt;p&gt;An event may remain in a Kafka topic, a dead-letter queue, or a replay mechanism long after the application version that originally created it has disappeared.&lt;/p&gt;

&lt;p&gt;That changes how we think about compatibility.&lt;/p&gt;

&lt;p&gt;Ordering deserves attention too.&lt;/p&gt;

&lt;p&gt;Kafka guarantees ordering &lt;strong&gt;within a partition&lt;/strong&gt;, not universal ordering across the entire system.&lt;/p&gt;

&lt;p&gt;The message key therefore becomes an architectural decision because it influences which events share a partition and which operations can be processed in parallel.&lt;/p&gt;

&lt;p&gt;Details that look small in producer code can have significant consequences for correctness and throughput.&lt;/p&gt;


&lt;h2&gt;
  
  
  Retry cannot be the final answer
&lt;/h2&gt;

&lt;p&gt;Some failures are temporary.&lt;/p&gt;

&lt;p&gt;A broker can become unavailable. A network request can time out. A dependency may take longer than expected to respond.&lt;/p&gt;

&lt;p&gt;Retry with backoff makes sense in those cases.&lt;/p&gt;

&lt;p&gt;But a structurally invalid event will still be invalid ten seconds later.&lt;/p&gt;

&lt;p&gt;An incompatible schema will not suddenly become compatible on attempt number 47.&lt;/p&gt;

&lt;p&gt;Retrying forever simply turns one bad message into a permanent consumer of CPU, logs, and operational attention.&lt;/p&gt;

&lt;p&gt;That is where a &lt;strong&gt;Dead Letter Queue&lt;/strong&gt;, or DLQ, becomes useful.&lt;/p&gt;

&lt;p&gt;A useful DLQ should preserve enough information for investigation and recovery: the original payload, event type, failure classification, source information, and correlation metadata.&lt;/p&gt;

&lt;p&gt;But putting something in a DLQ is only half the solution.&lt;/p&gt;

&lt;p&gt;The more important question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens next?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Can the message be safely replayed?&lt;/p&gt;

&lt;p&gt;Was the root cause fixed?&lt;/p&gt;

&lt;p&gt;Can a projection be rebuilt?&lt;/p&gt;

&lt;p&gt;Should the message be discarded?&lt;/p&gt;

&lt;p&gt;Does the operation require manual review?&lt;/p&gt;

&lt;p&gt;The project explores requeue, replay, and projection rebuild scenarios because recovery is part of the architecture too.&lt;/p&gt;

&lt;p&gt;A phrase I keep coming back to is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A DLQ without a recovery strategy is just a distributed archive of unresolved problems.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Transfers make distributed failures much easier to see
&lt;/h2&gt;

&lt;p&gt;Updating a read projection is relatively straightforward compared with an operation that produces multiple distributed side effects.&lt;/p&gt;

&lt;p&gt;Consider a transfer.&lt;/p&gt;

&lt;p&gt;At a simplified level, we need to create a debit and then a credit.&lt;/p&gt;

&lt;p&gt;Inside one local database transaction, ACID properties solve a lot of problems for us.&lt;/p&gt;

&lt;p&gt;Across independent components, those guarantees disappear.&lt;/p&gt;

&lt;p&gt;The project models this flow as an orchestrated Saga:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Transfer Requested] --&amp;gt; B[Create Debit]

    B --&amp;gt;|Success| C[Create Credit]
    B --&amp;gt;|Failure| F[Transfer Failed]

    C --&amp;gt;|Success| D[Transfer Completed]
    C --&amp;gt;|Failure| E[Compensate Debit]

    E --&amp;gt; G[Record Reversal]
    G --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This makes something very explicit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;when we distribute an operation, we also distribute its failure modes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the debit succeeds and the credit fails, there is no global &lt;code&gt;ROLLBACK&lt;/code&gt; statement capable of reversing time across independent services.&lt;/p&gt;

&lt;p&gt;We have to define what compensation means.&lt;/p&gt;

&lt;p&gt;In financial domains, compensation often should not erase the original fact. A reversal can instead create a new fact that neutralizes the previous one while preserving the history of what happened.&lt;/p&gt;

&lt;p&gt;A Saga does not recreate ACID across services.&lt;/p&gt;

&lt;p&gt;It models the states and compensating actions required because that local transaction boundary no longer exists.&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;


&lt;h2&gt;
  
  
  Circuit breakers do not hide failures. They control them.
&lt;/h2&gt;

&lt;p&gt;Another scenario in the laboratory intentionally takes &lt;code&gt;LedgerService&lt;/code&gt; offline while the transfer worker is still running.&lt;/p&gt;

&lt;p&gt;Without protection, the worker can keep calling a dependency that is already known to be unavailable.&lt;/p&gt;

&lt;p&gt;Retries can make this even worse by multiplying calls against an unhealthy service.&lt;/p&gt;

&lt;p&gt;A Circuit Breaker changes that behavior.&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Closed

    Closed: Requests flow normally
    Open: Requests fail fast
    HalfOpen: Limited probe requests

    Closed --&amp;gt; Open: Consecutive failures
    Open --&amp;gt; HalfOpen: Wait period expires
    HalfOpen --&amp;gt; Closed: Dependency recovered
    HalfOpen --&amp;gt; Open: Dependency still unhealthy&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When the circuit is open, the system has not somehow recovered.&lt;/p&gt;

&lt;p&gt;It has acknowledged the failure and chosen not to waste resources repeatedly exercising the same broken dependency.&lt;/p&gt;

&lt;p&gt;After a configured interval, the breaker allows a controlled probe through the half-open state.&lt;/p&gt;

&lt;p&gt;If that succeeds, traffic can resume.&lt;/p&gt;

&lt;p&gt;This leads to another principle that I think is worth emphasizing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilience does not mean making failures invisible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Resilience means making failure behavior predictable.&lt;/p&gt;


&lt;h2&gt;
  
  
  Observability becomes much more important after leaving HTTP
&lt;/h2&gt;

&lt;p&gt;Following a single HTTP request through logs is usually manageable.&lt;/p&gt;

&lt;p&gt;Following an asynchronous workflow across several processes is very different.&lt;/p&gt;

&lt;p&gt;A single operation may travel through:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Request[HTTP Request]
        --&amp;gt; API[Ledger API]
        --&amp;gt; DB[(PostgreSQL)]
        --&amp;gt; Outbox[Outbox]
        --&amp;gt; Worker[Ledger Worker]
        --&amp;gt; Kafka[(Kafka)]
        --&amp;gt; Consumer[Balance Worker]
        --&amp;gt; Projection[(Balance DB)]

    Request -. Correlation ID .-&amp;gt; API
    API -. Trace Context .-&amp;gt; Outbox
    Outbox -. traceparent .-&amp;gt; Worker
    Worker -. Trace Context .-&amp;gt; Kafka
    Kafka -. traceparent .-&amp;gt; Consumer&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If every component produces unrelated logs, diagnosing a problem becomes timestamp archaeology.&lt;/p&gt;

&lt;p&gt;The project propagates &lt;code&gt;correlation_id&lt;/code&gt;, and when OpenTelemetry is enabled, W3C tracing context such as &lt;code&gt;traceparent&lt;/code&gt; and &lt;code&gt;tracestate&lt;/code&gt; can also travel through the Outbox and Kafka messages.&lt;/p&gt;

&lt;p&gt;The observability side can be viewed separately:&lt;br&gt;
&lt;/p&gt;
&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Client[Client] --&amp;gt; API[Ledger API]
    API --&amp;gt; DB[(PostgreSQL)]
    DB --&amp;gt; Outbox[Outbox]
    Outbox --&amp;gt; Worker[Ledger Worker]
    Worker --&amp;gt; Kafka[(Kafka)]
    Kafka --&amp;gt; Consumer[Balance Worker]
    Consumer --&amp;gt; Balance[(Balance DB)]

    API -.-&amp;gt; OTEL[OpenTelemetry]
    Worker -.-&amp;gt; OTEL
    Consumer -.-&amp;gt; OTEL

    OTEL --&amp;gt; Traces[Traces]
    OTEL --&amp;gt; Metrics[Metrics]

    API -. Logs .-&amp;gt; Logs[Centralized Logs]
    Worker -. Logs .-&amp;gt; Logs
    Consumer -. Logs .-&amp;gt; Logs&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The local stack includes OpenTelemetry, Jaeger, Prometheus, Grafana, Loki, and supporting components.&lt;/p&gt;

&lt;p&gt;But the tools are not the important part.&lt;/p&gt;

&lt;p&gt;The important part is the questions they allow us to answer.&lt;/p&gt;

&lt;p&gt;The API is healthy, but is the Outbox backlog growing?&lt;/p&gt;

&lt;p&gt;Is the producer failing?&lt;/p&gt;

&lt;p&gt;Is consumer processing slowing down?&lt;/p&gt;

&lt;p&gt;Are duplicates increasing?&lt;/p&gt;

&lt;p&gt;Has the DLQ started receiving messages?&lt;/p&gt;

&lt;p&gt;Which original HTTP request produced the event that failed several minutes later in another process?&lt;/p&gt;

&lt;p&gt;Once asynchronous processing becomes central to the system, an HTTP health endpoint tells only a very small part of the story.&lt;/p&gt;


&lt;h2&gt;
  
  
  Test the architecture, not only the classes
&lt;/h2&gt;

&lt;p&gt;A system like this can have excellent unit-test coverage and still fail exactly where the interesting risks are: between components.&lt;/p&gt;

&lt;p&gt;That is why the laboratory also contains integration tests and k6 scenarios that exercise complete workflows.&lt;/p&gt;

&lt;p&gt;One scenario validates:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ledger
  -&amp;gt; Outbox
  -&amp;gt; Kafka
  -&amp;gt; Balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Another exercises the complete Transfer Saga.&lt;/p&gt;

&lt;p&gt;There is also a resilience scenario in which the Ledger dependency is deliberately stopped so the Circuit Breaker's behavior can be observed during failure and recovery.&lt;/p&gt;

&lt;p&gt;I am careful not to describe these tests as production-scale benchmarks.&lt;/p&gt;

&lt;p&gt;They run in a controlled local environment.&lt;/p&gt;

&lt;p&gt;Their latency thresholds are regression guardrails, not production SLOs.&lt;/p&gt;

&lt;p&gt;Running 50 requests per second on Docker Compose does not prove that an architecture can operate at banking scale.&lt;/p&gt;

&lt;p&gt;Real capacity depends on infrastructure, partitions, database behavior, network topology, autoscaling, workload characteristics, failure modes, and many other variables.&lt;/p&gt;

&lt;p&gt;The tests prove something narrower, but still valuable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the architectural behavior remains testable under concurrency and controlled failure conditions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the best architecture test is not checking whether a certain method was called.&lt;/p&gt;

&lt;p&gt;It is turning a dependency off and observing what the system actually does.&lt;/p&gt;


&lt;h2&gt;
  
  
  This is a laboratory, not a production reference architecture
&lt;/h2&gt;

&lt;p&gt;Projects that demonstrate microservices, Kafka, Outbox, Sagas, and observability can easily give the impression that they represent a universal production blueprint.&lt;/p&gt;

&lt;p&gt;This one does not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;poc-arquitetura&lt;/code&gt; is intentionally a laboratory for experimenting with decisions.&lt;/p&gt;

&lt;p&gt;A real production environment would still need serious work around secrets management, workload identity, high availability, Kafka capacity and replication, disaster recovery, network security, production SLOs, infrastructure topology, deployment strategy, and many domain-specific controls.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Software architecture is not the process of fitting as many patterns as possible into a diagram.&lt;/p&gt;

&lt;p&gt;It is the process of choosing mechanisms that are proportional to the problems the system actually has.&lt;/p&gt;


&lt;h2&gt;
  
  
  Quick takeaways
&lt;/h2&gt;

&lt;p&gt;The database commit is often only the beginning of a distributed workflow.&lt;/p&gt;

&lt;p&gt;Transactional Outbox does not make PostgreSQL and Kafka a single transaction; it makes the intent to publish durable.&lt;/p&gt;

&lt;p&gt;At-least-once delivery means duplicates are expected, which makes idempotency a correctness requirement rather than a nice optimization.&lt;/p&gt;

&lt;p&gt;Eventual consistency has to be understood by the product, not hidden behind a message broker.&lt;/p&gt;

&lt;p&gt;Retries require operations that are safe to repeat. DLQs require recovery procedures. Sagas make states and compensations explicit when a local transaction is no longer available.&lt;/p&gt;

&lt;p&gt;Circuit Breakers control failures instead of pretending those failures disappeared.&lt;/p&gt;

&lt;p&gt;And observability has to cross the same boundaries that events cross.&lt;/p&gt;

&lt;p&gt;If I had to reduce the whole experiment to one idea, it would be this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In distributed systems, we do not design only the successful path. We also design how failures are detected, understood, and recovered.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;For a broader view of .NET microservice architecture, Microsoft's &lt;strong&gt;“.NET Microservices: Architecture for Containerized .NET Applications”&lt;/strong&gt; is a useful starting point.&lt;/p&gt;

&lt;p&gt;For Transactional Outbox, Saga, Idempotent Consumer, and related distributed-system patterns, &lt;strong&gt;Chris Richardson's Microservices Patterns&lt;/strong&gt; and the Microservices.io pattern catalog are excellent references.&lt;/p&gt;

&lt;p&gt;For Kafka, it is worth going beyond basic producer/consumer tutorials and studying the official material on &lt;strong&gt;partitions, consumer groups, offsets, ordering, delivery semantics, and transactions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For distributed tracing and context propagation, the &lt;strong&gt;OpenTelemetry documentation&lt;/strong&gt; provides a vendor-neutral mental model for traces, spans, metrics, baggage, and propagation.&lt;/p&gt;

&lt;p&gt;For load and resilience testing, &lt;strong&gt;Grafana k6&lt;/strong&gt; is approachable enough to start small while still supporting serious workloads.&lt;/p&gt;

&lt;p&gt;And if you want to understand the deeper ideas behind transactions, replication, streams, partitioning, and distributed data, &lt;strong&gt;Designing Data-Intensive Applications&lt;/strong&gt;, by Martin Kleppmann, is still one of the books I would put near the top of the list.&lt;/p&gt;

&lt;p&gt;The repository behind this article contains the source code, ADRs, event contracts, architecture documentation, tests, and failure scenarios discussed here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rodri-oliveira-dev" rel="noopener noreferrer"&gt;
        rodri-oliveira-dev
      &lt;/a&gt; / &lt;a href="https://github.com/rodri-oliveira-dev/poc-arquitetura" rel="noopener noreferrer"&gt;
        poc-arquitetura
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      .NET architecture PoC for distributed services with CQRS, Kafka, PostgreSQL, Outbox, DLQ, observability and CI quality gates.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;poc-arquitetura&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/rodri-oliveira-dev/poc-arquitetura/actions/workflows/dotnet.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fe3131f0ae74307a8b6fee0b1aef474c4350a03abbe765f7bc5c23de95e7b9d5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f6472692d6f6c6976656972612d6465762f706f632d61727175697465747572612f646f746e65742e796d6c3f6272616e63683d6d61696e266c6162656c3d6275696c64" alt="Build"&gt;&lt;/a&gt;
&lt;a href="https://github.com/rodri-oliveira-dev/poc-arquitetura/actions/workflows/dotnet.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/243e7d62ebda78d9a12d30c0d637d34ba406b27d4ae140b0dc40ec6c02c2365a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f6472692d6f6c6976656972612d6465762f706f632d61727175697465747572612f646f746e65742e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473" alt="Tests"&gt;&lt;/a&gt;
&lt;a href="https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_poc-arquitetura" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1cbca7d4eb2ec66bbc0533942842415c0e083cffef775ff5bddff0b05392e71f/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d726f6472692d6f6c6976656972612d6465765f706f632d6172717569746574757261266d65747269633d616c6572745f737461747573" alt="Quality Gate Status"&gt;&lt;/a&gt;
&lt;a href="https://sonarcloud.io/summary/new_code?id=rodri-oliveira-dev_poc-arquitetura" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/efb4081b14125ef862be0bab0ceb1c09bfd3cf406d3f06487b8eb0ee6391ac36/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d726f6472692d6f6c6976656972612d6465765f706f632d6172717569746574757261266d65747269633d73656375726974795f726174696e67" alt="Security Rating"&gt;&lt;/a&gt;
&lt;a href="https://rodri-oliveira-dev.github.io/poc-arquitetura/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e03cfc430fac7d97d80b41b3e219c9467a412ba45611163352c3e30055b39c09/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f6472692d6f6c6976656972612d6465762f706f632d61727175697465747572612f70616765732d6172636869746563747572652e796d6c3f6272616e63683d6d61696e266c6162656c3d617263686974656374757265253230646f6373" alt="Architecture Docs"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;POC educacional de microserviços em .NET para estudar arquitetura de software com código real: Clean Architecture, DDD, PostgreSQL, Kafka, Outbox, Inbox, JWT/JWKS com Keycloak, observabilidade, segurança, contratos e testes automatizados.&lt;/p&gt;
&lt;p&gt;Ela demonstra um problema comum em sistemas financeiros: registrar fatos de forma transacional, publicar eventos com confiabilidade, projetar saldos em outro serviço e operar falhas sem esconder consistência eventual. O repositório também mostra contextos de identidade, transferência, pagamento externo e auditoria funcional para exercitar trade-offs de integração.&lt;/p&gt;
&lt;p&gt;Este projeto é útil para:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;quem está aprendendo arquitetura e quer ver os conceitos aplicados;&lt;/li&gt;
&lt;li&gt;desenvolvedores .NET que querem executar, testar e alterar uma stack local;&lt;/li&gt;
&lt;li&gt;arquitetos que querem avaliar decisões, limites e riscos;&lt;/li&gt;
&lt;li&gt;avaliadores técnicos que querem entender a proposta rapidamente.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Visão geral&lt;/h2&gt;
&lt;/div&gt;

  &lt;div class="js-render-enrichment-target"&gt;
    &lt;div class="render-plaintext-hidden"&gt;
      &lt;pre&gt;flowchart LR
    Client[Cliente ou teste] --&amp;gt; Keycloak[Keycloak OIDC]
    Client --&amp;gt; LedgerApi[LedgerService.Api]
    Client --&amp;gt; BalanceApi[BalanceService.Api]
    Client --&amp;gt; TransferApi[TransferService.Api]
    Client --&amp;gt; PaymentApi[PaymentService.Api]
    Client --&amp;gt; IdentityApi[IdentityService.Api]
    Client --&amp;gt; AuditApi[AuditService.Api]
    LedgerApi --&amp;gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rodri-oliveira-dev/poc-arquitetura" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;There is still plenty I want to experiment with.&lt;/p&gt;

&lt;p&gt;And that is probably the part I enjoy most about this kind of architecture work: once failure stops being treated as an exceptional event and becomes part of the design, the questions get much more interesting.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>architecture</category>
      <category>dotnet</category>
      <category>kafka</category>
    </item>
    <item>
      <title>How I organized security policies, reusable workflows, .NET maintenance, and AI agent governance across my GitHub repositories.</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/how-i-organized-security-policies-reusable-workflows-net-maintenance-and-ai-agent-governance-4jgg</link>
      <guid>https://dev.to/rodri-oliveira-dev/how-i-organized-security-policies-reusable-workflows-net-maintenance-and-ai-agent-governance-4jgg</guid>
      <description>&lt;h2&gt;
  
  
  The challenge of maintaining multiple repositories
&lt;/h2&gt;

&lt;p&gt;When you maintain one or two repositories, duplicating a few configuration files doesn't seem like a big deal.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; here, a pull request template there, some security policies, and a few GitHub Actions workflows. Everything works, and development continues.&lt;/p&gt;

&lt;p&gt;The problem becomes more noticeable as the number of repositories grows.&lt;/p&gt;

&lt;p&gt;A security improvement implemented in one project never reaches the others. Workflows evolve, but several repositories continue using older versions. Contribution guidelines become inconsistent, and dependencies require individual updates.&lt;/p&gt;

&lt;p&gt;Over time, these differences create configuration drift.&lt;/p&gt;

&lt;p&gt;I started addressing this problem by evolving my &lt;code&gt;.github&lt;/code&gt; repository into a central governance and automation layer for the projects I maintain.&lt;/p&gt;

&lt;p&gt;The goal wasn't to move every configuration into a single repository or eliminate each project's autonomy. I wanted to establish shared standards, reduce duplication, and automate recurring maintenance tasks while preserving repository-specific requirements.&lt;/p&gt;

&lt;p&gt;In this article, I'll explain how that structure works, what I implemented, and the architectural decisions behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a &lt;code&gt;.github&lt;/code&gt; repository special?
&lt;/h2&gt;

&lt;p&gt;GitHub provides special behavior for public repositories named &lt;code&gt;.github&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They can contain default community health files that GitHub uses across other public repositories belonging to the same account or organization.&lt;/p&gt;

&lt;p&gt;This includes files such as &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, &lt;code&gt;CODE_OF_CONDUCT.md&lt;/code&gt;, &lt;code&gt;SECURITY.md&lt;/code&gt;, &lt;code&gt;SUPPORT.md&lt;/code&gt;, issue templates, and pull request templates.&lt;/p&gt;

&lt;p&gt;When a repository doesn't provide its own version of a supported file, GitHub can use the corresponding default.&lt;/p&gt;

&lt;p&gt;The important part is that local configurations take precedence.&lt;/p&gt;

&lt;p&gt;If a project requires a different security policy or pull request template, it can maintain its own version.&lt;/p&gt;

&lt;p&gt;This creates a useful governance model: shared defaults with local overrides when necessary.&lt;/p&gt;

&lt;p&gt;I initially used this functionality to provide consistent contribution guidelines, security policies, and support documentation.&lt;/p&gt;

&lt;p&gt;As my projects evolved, I realized several other responsibilities could benefit from a similar approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  From shared files to a governance layer
&lt;/h2&gt;

&lt;p&gt;Maintaining multiple repositories involves more than keeping documentation consistent.&lt;/p&gt;

&lt;p&gt;Dependency updates, SDK versions, security checks, pipeline validation, and recurring maintenance tasks often require similar implementations.&lt;/p&gt;

&lt;p&gt;Instead of handling each concern independently, I started using the &lt;code&gt;.github&lt;/code&gt; repository as a coordination point.&lt;/p&gt;

&lt;p&gt;Today, it includes community health files, reusable workflows, maintenance automation, security policies, validation scripts, and a versioned registry of instructions and skills for development agents.&lt;/p&gt;

&lt;p&gt;There is an important distinction here.&lt;/p&gt;

&lt;p&gt;GitHub doesn't automatically distribute every file stored in a &lt;code&gt;.github&lt;/code&gt; repository to other projects.&lt;/p&gt;

&lt;p&gt;Native inheritance applies only to specific supported features. Workflows, Dependabot configurations, custom scripts, and AI agent instructions require their own reuse or distribution mechanisms.&lt;/p&gt;

&lt;p&gt;My implementation combines native GitHub features with automation built using GitHub Actions and the GitHub API.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.github&lt;/code&gt; repository provides the foundation, but the broader governance model is something we build on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable workflows and centralized policies
&lt;/h2&gt;

&lt;p&gt;One of the first responsibilities I centralized was the implementation of policies used by multiple CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;GitHub Actions supports reusable workflows, allowing one workflow to call another.&lt;/p&gt;

&lt;p&gt;Imagine several repositories that need to perform the same security scan.&lt;/p&gt;

&lt;p&gt;Instead of maintaining independent copies of the scanning logic, we can expose a reusable workflow that each project calls from its own pipeline.&lt;/p&gt;

&lt;p&gt;A simplified example:&lt;/p&gt;

&lt;p&gt;YAML&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner/.github/.github/workflows/reusable-secret-scan.yml@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;owner&lt;/code&gt; represents the account maintaining the central repository, and &lt;code&gt;v1&lt;/code&gt; represents a published version of the reusable workflow.&lt;/p&gt;

&lt;p&gt;The consuming repository decides when the scan runs and how it fits into its development process.&lt;/p&gt;

&lt;p&gt;The central repository maintains the shared implementation.&lt;/p&gt;

&lt;p&gt;The application still owns its pipeline. The central repository owns the shared policy implementation.&lt;/p&gt;

&lt;p&gt;In my project, one example is a reusable secret-scanning workflow designed to detect potential credentials and other sensitive information in Git history, independently of the programming language used by the consumer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why versioning matters
&lt;/h3&gt;

&lt;p&gt;When multiple repositories depend on a shared workflow, changes can affect several consumers.&lt;/p&gt;

&lt;p&gt;Pointing every repository to the central &lt;code&gt;main&lt;/code&gt; branch may be convenient, but it creates a dependency on its latest state.&lt;/p&gt;

&lt;p&gt;That's why I implemented an explicit versioning and release process.&lt;/p&gt;

&lt;p&gt;The strategy supports semantic versioning, major-version references, and full commit SHA references when strict reproducibility is required.&lt;/p&gt;

&lt;p&gt;A major-version reference allows consumers to receive compatible updates within that release channel. A full commit SHA identifies the exact code that will execute.&lt;/p&gt;

&lt;p&gt;Reusing code doesn't eliminate coupling. It requires us to manage that coupling deliberately.&lt;/p&gt;

&lt;p&gt;Versioning, compatibility, and change review are essential parts of maintaining shared automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing the automation itself
&lt;/h2&gt;

&lt;p&gt;When a repository starts coordinating operations across other repositories, its role changes.&lt;/p&gt;

&lt;p&gt;It can execute code, access repository metadata, create branches, and open pull requests.&lt;/p&gt;

&lt;p&gt;This introduces additional security considerations.&lt;/p&gt;

&lt;p&gt;One practice I adopted was pinning external GitHub Actions to their full commit SHA, identifying the exact code being executed.&lt;/p&gt;

&lt;p&gt;I also configured Dependabot to monitor GitHub Actions dependencies and propose updates through pull requests.&lt;/p&gt;

&lt;p&gt;However, automated updates don't mean automated merges.&lt;/p&gt;

&lt;p&gt;Updating an Action changes the code executed by a pipeline. Depending on the workflow, that code may have access to tokens, secrets, or other sensitive resources.&lt;/p&gt;

&lt;p&gt;For this reason, human review remains part of the process.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Apps and least privilege
&lt;/h3&gt;

&lt;p&gt;I also use a GitHub App for operations involving multiple repositories.&lt;/p&gt;

&lt;p&gt;Instead of relying on a long-lived personal access token with broad permissions, the automation uses short-lived installation access tokens.&lt;/p&gt;

&lt;p&gt;This allows permissions to be restricted to the operations the application actually needs.&lt;/p&gt;

&lt;p&gt;For example, my .NET repository inventory separates repository discovery from code inspection.&lt;/p&gt;

&lt;p&gt;The discovery stage uses credentials to identify eligible repositories. The inspection stage receives only the information needed to process selected public repositories, without receiving the GitHub App's private key.&lt;/p&gt;

&lt;p&gt;This reduces credential exposure during code inspection.&lt;/p&gt;

&lt;p&gt;It's a practical application of the principle of least privilege: each component receives only the access required to perform its responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating .NET repository maintenance
&lt;/h2&gt;

&lt;p&gt;Beyond shared policies, I implemented automation to help maintain my .NET repositories.&lt;/p&gt;

&lt;p&gt;One example is SDK version synchronization.&lt;/p&gt;

&lt;p&gt;The workflow identifies eligible repositories, checks their root-level &lt;code&gt;global.json&lt;/code&gt; files, and determines whether an update is available under the configured compatibility policy.&lt;/p&gt;

&lt;p&gt;When an applicable update is found, it prepares the change in a branch and opens a pull request.&lt;/p&gt;

&lt;p&gt;The automation doesn't merge directly into the default branch. Each repository retains its normal review and integration process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a centralized .NET inventory
&lt;/h3&gt;

&lt;p&gt;Another workflow produces an inventory of .NET projects across repositories accessible to the configured GitHub App.&lt;/p&gt;

&lt;p&gt;The goal isn't to modify projects but to obtain a consolidated view of their metadata.&lt;/p&gt;

&lt;p&gt;The inventory uses DotNetRepoInspector to inspect .NET repositories using effective MSBuild metadata rather than relying exclusively on direct &lt;code&gt;.csproj&lt;/code&gt; parsing.&lt;/p&gt;

&lt;p&gt;This integration illustrates an architectural principle I wanted to preserve.&lt;/p&gt;

&lt;p&gt;Each project retains a specific responsibility.&lt;/p&gt;

&lt;p&gt;DotNetRepoInspector performs the inspection. The &lt;code&gt;.github&lt;/code&gt; repository coordinates execution across multiple repositories and consolidates the results.&lt;/p&gt;

&lt;p&gt;I didn't need to move the inspection implementation into the central repository to reuse its capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation needs engineering, too
&lt;/h2&gt;

&lt;p&gt;Maintenance scripts often start small.&lt;/p&gt;

&lt;p&gt;A few lines of Bash solve a problem, become part of a workflow, and continue running for months.&lt;/p&gt;

&lt;p&gt;Things change when that same script starts modifying multiple repositories.&lt;/p&gt;

&lt;p&gt;At that point, it deserves the same engineering attention we give other software components.&lt;/p&gt;

&lt;p&gt;That's why my &lt;code&gt;.github&lt;/code&gt; repository includes regression tests, contract validation, and quality checks using tools such as &lt;code&gt;actionlint&lt;/code&gt; and &lt;code&gt;ShellCheck&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also implemented mechanisms for handling failures during batch operations.&lt;/p&gt;

&lt;p&gt;If an update fails for one repository, processing can continue for the remaining repositories when it's safe to do so. Failures must still be explicitly reported.&lt;/p&gt;

&lt;p&gt;Continuing after a recoverable error shouldn't mean hiding that error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automation branch ownership
&lt;/h3&gt;

&lt;p&gt;Another concern involves the ownership of branches created by automation.&lt;/p&gt;

&lt;p&gt;The existence of a branch named &lt;code&gt;chore/sync-dotnet-sdk&lt;/code&gt; doesn't automatically mean a workflow can assume that branch belongs to it.&lt;/p&gt;

&lt;p&gt;Someone may have created it manually, or it may contain changes that shouldn't be overwritten.&lt;/p&gt;

&lt;p&gt;For that reason, the automation performs additional ownership and pull request state checks before reusing or updating an existing branch.&lt;/p&gt;

&lt;p&gt;These safeguards become particularly relevant when scripts can modify multiple repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing shared instructions for AI development agents
&lt;/h2&gt;

&lt;p&gt;More recently, I encountered another form of shared configuration: instructions used by AI development agents.&lt;/p&gt;

&lt;p&gt;Tools capable of inspecting code, implementing issues, and reviewing pull requests need to understand the rules and conventions of the projects they work on.&lt;/p&gt;

&lt;p&gt;These instructions may cover architecture, testing strategies, security policies, and implementation procedures.&lt;/p&gt;

&lt;p&gt;When maintaining multiple repositories, some instructions naturally overlap.&lt;/p&gt;

&lt;p&gt;Copying the same files between projects, however, creates the same configuration drift problem we see with workflows.&lt;/p&gt;

&lt;p&gt;To address this, I created an &lt;code&gt;agent-governance&lt;/code&gt; directory inside the central repository.&lt;/p&gt;

&lt;p&gt;It contains shared instructions, profiles, reusable skills, a manifest, and its own version file.&lt;/p&gt;

&lt;p&gt;The available skills cover activities such as bug investigation, issue implementation, pull request review, security analysis, refactoring, and test coverage analysis.&lt;/p&gt;

&lt;p&gt;The goal is to provide shared instructions while allowing individual projects to extend them according to their specific needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distributing instructions to consuming repositories
&lt;/h3&gt;

&lt;p&gt;Unlike community health files, AI agent instructions aren't automatically inherited from the &lt;code&gt;.github&lt;/code&gt; repository.&lt;/p&gt;

&lt;p&gt;A separate process is required to manage their evolution and distribution.&lt;/p&gt;

&lt;p&gt;I implemented workflows that validate the central registry, synchronize selected skills from a controlled upstream source, and distribute approved updates to consuming repositories.&lt;/p&gt;

&lt;p&gt;When the automation identifies a difference between the approved central version and the version used by a consumer, it can prepare a pull request containing the update.&lt;/p&gt;

&lt;p&gt;This preserves change review, version history, and the ability to maintain repository-specific instructions.&lt;/p&gt;

&lt;p&gt;The same principle used for reusable workflows applies here:&lt;/p&gt;

&lt;p&gt;Centralize what is shared without removing control from the consuming projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do we gain from this approach?
&lt;/h2&gt;

&lt;p&gt;The main benefit isn't simply having fewer files.&lt;/p&gt;

&lt;p&gt;It's reducing the number of places where the same decision must be maintained.&lt;/p&gt;

&lt;p&gt;When a security policy has several independent copies, each copy can evolve differently.&lt;/p&gt;

&lt;p&gt;With a central source and a controlled mechanism for reuse or distribution, the problem changes.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I keep ten configurations identical?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I maintain one shared policy while preserving compatibility with its consumers?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second problem isn't trivial, but it's more explicit and easier to manage systematically.&lt;/p&gt;

&lt;p&gt;It also becomes easier to distinguish responsibilities that belong to the broader repository ecosystem from those that should remain within individual projects.&lt;/p&gt;

&lt;p&gt;Not every configuration should be centralized.&lt;/p&gt;

&lt;p&gt;A workflow supporting a specific application's deployment process may have little value for other repositories. Similarly, some AI agent instructions may depend on architectural decisions unique to a particular project.&lt;/p&gt;

&lt;p&gt;Centralizing everything can introduce unnecessary coupling.&lt;/p&gt;

&lt;p&gt;That's why I treat the &lt;code&gt;.github&lt;/code&gt; repository as a shared governance layer rather than a universal configuration repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs of centralization
&lt;/h2&gt;

&lt;p&gt;A centralized structure introduces responsibilities of its own.&lt;/p&gt;

&lt;p&gt;An incorrect change to a shared policy may affect multiple repositories. An incompatible workflow update can break consuming pipelines. Automation with excessive permissions can increase the impact of a security incident.&lt;/p&gt;

&lt;p&gt;This is why versioning, validation, review, and access control need to evolve alongside centralization.&lt;/p&gt;

&lt;p&gt;Different types of configuration also require different sharing mechanisms.&lt;/p&gt;

&lt;p&gt;Community health files can use GitHub's native default-file behavior. Reusable workflows must be explicitly called by consumers. Other configurations require dedicated distribution processes.&lt;/p&gt;

&lt;p&gt;Each mechanism has its own limitations.&lt;/p&gt;

&lt;p&gt;The goal isn't to eliminate every difference between projects. It's to make sure those differences are intentional rather than the result of neglected maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;Building this repository reinforced an important architectural principle: centralization and autonomy don't have to be opposing concepts.&lt;/p&gt;

&lt;p&gt;We can establish shared standards without preventing individual projects from making their own decisions.&lt;/p&gt;

&lt;p&gt;Reuse also requires governance. Shared workflows need versioning, automation needs testing, and distribution mechanisms need traceability and review.&lt;/p&gt;

&lt;p&gt;Security deserves particular attention when a tool operates across multiple repositories. Short-lived credentials, restricted permissions, and separation of responsibilities help reduce the risks associated with centralized automation.&lt;/p&gt;

&lt;p&gt;Finally, AI development instructions should be treated like other engineering artifacts. They require maintenance, versioning, and controlled update mechanisms.&lt;/p&gt;

&lt;p&gt;The main lesson is simple: centralize policies that are genuinely shared, and keep project-specific decisions within the projects that own them.&lt;/p&gt;

&lt;p&gt;Even a personal portfolio can provide a practical environment for exploring Platform Engineering, Developer Experience, and software governance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;If you want to explore these concepts, the official GitHub documentation is a good starting point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file" rel="noopener noreferrer"&gt;Creating a default community health file&lt;/a&gt;&amp;nbsp; explains which files can be shared through a &lt;code&gt;.github&lt;/code&gt; repository and how local configurations take precedence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/sharing-automations/reusing-workflows" rel="noopener noreferrer"&gt;Reusing workflows&lt;/a&gt;&amp;nbsp; covers reusable GitHub Actions workflows, including inputs, secrets, outputs, and reuse limitations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;Secure use reference&lt;/a&gt;&amp;nbsp; describes security considerations for GitHub Actions, including permissions, credentials, and third-party Actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/apps/creating-github-apps" rel="noopener noreferrer"&gt;Creating GitHub Apps&lt;/a&gt;&amp;nbsp; provides information about application authentication, installation permissions, and access tokens.&lt;/p&gt;

&lt;p&gt;For a practical implementation, explore the projects linked at the beginning of this article. Their code and documentation are publicly available for anyone who wants to study the implementation, adapt individual components, or contribute.&lt;/p&gt;

&lt;p&gt;If you found this project useful or it gave you an idea for your own repositories, consider leaving a ⭐ on the &lt;a href="https://github.com/rodri-oliveira-dev/.github" rel="noopener noreferrer"&gt;&lt;code&gt;.github&lt;/code&gt; repository&lt;/a&gt;&amp;nbsp;. It's a simple way to support the project.&lt;/p&gt;

&lt;p&gt;Projects covered in this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/rodri-oliveira-dev/.github" rel="noopener noreferrer"&gt;&lt;code&gt;.github&lt;/code&gt; — Shared governance and automation&lt;/a&gt;&amp;nbsp;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/rodri-oliveira-dev/DotNetRepoInspector" rel="noopener noreferrer"&gt;DotNetRepoInspector — .NET repository inspection and inventory&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>githubactions</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>ADR: Recording the Why Behind Decisions and How ADR Guard Helps Keep Architectural History Reliable</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/adr-recording-the-why-behind-decisions-and-how-adr-guard-helps-keep-architectural-history-reliable-10cb</link>
      <guid>https://dev.to/rodri-oliveira-dev/adr-recording-the-why-behind-decisions-and-how-adr-guard-helps-keep-architectural-history-reliable-10cb</guid>
      <description>&lt;p&gt;In projects that live for more than a few years, there is one question that comes up surprisingly often:&lt;/p&gt;

&lt;p&gt;“Why did we build it this way?”&lt;/p&gt;

&lt;p&gt;Maybe it was the decision to choose PostgreSQL instead of MongoDB. Maybe the team decided to use messaging, adopt microservices, keep a modular monolith, introduce distributed caching, or standardize an observability strategy.&lt;/p&gt;

&lt;p&gt;The code usually tells us &lt;strong&gt;what was implemented&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Git tells us &lt;strong&gt;when it changed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But neither necessarily explains &lt;strong&gt;why the decision was made&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is exactly where Architecture Decision Records, or simply &lt;strong&gt;ADRs&lt;/strong&gt;, come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not forgetting the technology. It is forgetting the context
&lt;/h2&gt;

&lt;p&gt;Architectural decisions rarely happen in a vacuum.&lt;/p&gt;

&lt;p&gt;Imagine that, a few years ago, a team decided not to use a particular technology. Someone joining the project today might look at the architecture and think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This doesn’t make sense. We could simplify all of this by using X.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe they could.&lt;/p&gt;

&lt;p&gt;But perhaps X had been rejected because the system needed to satisfy a regulatory constraint. Maybe the team did not yet have enough operational maturity. Maybe licensing was a problem. Or perhaps, at the time, the technology simply did not meet an important requirement.&lt;/p&gt;

&lt;p&gt;Without that context, old decisions can look arbitrary.&lt;/p&gt;

&lt;p&gt;Michael Nygard helped popularize ADRs around this exact idea: useful architecture documentation does not have to be a massive document describing the entire system. Small, independent records that explain individual decisions have a much better chance of remaining understandable and useful over time.&lt;/p&gt;

&lt;p&gt;An ADR is essentially a &lt;strong&gt;snapshot of architectural reasoning at a particular point in time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It does not try to explain the entire architecture.&lt;/p&gt;

&lt;p&gt;It explains one decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes into an ADR?
&lt;/h2&gt;

&lt;p&gt;There are several ADR templates, but one of the best-known formats is intentionally simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt; explains the problem, constraints, and forces that created the need for a decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision&lt;/strong&gt; records what was decided.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequences&lt;/strong&gt; describe what changes because of that choice, including benefits, costs, and trade-offs.&lt;/p&gt;

&lt;p&gt;There is usually also a &lt;strong&gt;Status&lt;/strong&gt;, indicating whether the decision is being proposed, has been accepted, has been deprecated, or has been superseded by another decision.&lt;/p&gt;

&lt;p&gt;AWS, for example, recommends using ADRs for architecturally significant decisions involving system structure, non-functional requirements, dependencies, interfaces, and construction techniques.&lt;/p&gt;

&lt;p&gt;That distinction matters because an ADR should not become a development diary.&lt;/p&gt;

&lt;p&gt;“We upgraded a library version” probably does not need an ADR.&lt;/p&gt;

&lt;p&gt;“We standardized every service in the organization on a specific authentication strategy” probably does.&lt;/p&gt;

&lt;p&gt;A useful question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this decision significantly affect how the system will be built, operated, or evolved?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is yes, there is probably an architectural decision worth recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  ADRs are less about documentation and more about institutional memory
&lt;/h2&gt;

&lt;p&gt;As ADRs accumulate, they form what is often called a &lt;strong&gt;decision log&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That history becomes especially valuable as people join and leave teams.&lt;/p&gt;

&lt;p&gt;Instead of depending on the one developer who “remembers why we did that in 2023,” the context becomes part of the repository itself.&lt;/p&gt;

&lt;p&gt;There is another benefit that is less obvious: ADRs reduce repeated discussions.&lt;/p&gt;

&lt;p&gt;Without a record, a team can debate the same architectural question multiple times over the years. Someone proposes a technology, the team discusses it, decides against it, and six months later someone else reopens exactly the same discussion because the original reasoning was never documented.&lt;/p&gt;

&lt;p&gt;With an ADR, the conversation can start several steps ahead:&lt;/p&gt;

&lt;p&gt;“We made this decision for these reasons. Have any of those assumptions changed?”&lt;/p&gt;

&lt;p&gt;That is a much better discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the ADRs themselves become a problem
&lt;/h2&gt;

&lt;p&gt;Adopting ADRs sounds easy: create a &lt;code&gt;docs/adr&lt;/code&gt; directory, add a few Markdown files, and you are done.&lt;/p&gt;

&lt;p&gt;In practice, small inconsistencies begin to appear over time.&lt;/p&gt;

&lt;p&gt;One file is called &lt;code&gt;0003-use-redis.md&lt;/code&gt;, another is called &lt;code&gt;adr4-rabbitmq.md&lt;/code&gt;. One ADR uses the status &lt;code&gt;Approved&lt;/code&gt;, another uses &lt;code&gt;Accepted&lt;/code&gt;. Some decisions have no consequences section. Two files accidentally reuse the same ID. One ADR says it was superseded by another decision, but the referenced file no longer exists.&lt;/p&gt;

&lt;p&gt;And perhaps the most common problem: someone creates a manually maintained ADR index that stops being updated after a few months.&lt;/p&gt;

&lt;p&gt;Individually, none of these problems seems particularly serious.&lt;/p&gt;

&lt;p&gt;Together, however, they slowly make the documentation less trustworthy.&lt;/p&gt;

&lt;p&gt;That is exactly the space &lt;strong&gt;&lt;a href="https://github.com/rodri-oliveira-dev/adr-guard" rel="noopener noreferrer"&gt;ADR Guard&lt;/a&gt;&lt;/strong&gt; is designed to address.&lt;/p&gt;

&lt;h2&gt;
  
  
  ADR Guard: treating ADRs as verifiable artifacts
&lt;/h2&gt;

&lt;p&gt;ADR Guard is a .NET command-line tool for validating and indexing Architecture Decision Records.&lt;/p&gt;

&lt;p&gt;The idea is straightforward: if certain conventions matter to your architectural documentation, they do not have to remain informal recommendations buried in a README.&lt;/p&gt;

&lt;p&gt;They can become &lt;strong&gt;automatically verifiable invariants&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By default, ADR Guard expects files such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0001-use-postgresql.md
0002-adopt-opentelemetry.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a structure based on the classic ADR format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Use PostgreSQL&lt;/span&gt;

&lt;span class="gu"&gt;## Status&lt;/span&gt;

Accepted

&lt;span class="gu"&gt;## Context&lt;/span&gt;

We need a relational database.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

Use PostgreSQL.

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

The team will need operational knowledge of PostgreSQL.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adr-guard check docs/adr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;analyzes the records and validates filenames, IDs, titles, statuses, required sections, and relationships between ADRs.&lt;/p&gt;

&lt;p&gt;The tool currently exposes stable validation codes from &lt;code&gt;ADR001&lt;/code&gt; through &lt;code&gt;ADR009&lt;/code&gt;, covering problems such as duplicate IDs, broken relative references, missing required sections, and &lt;code&gt;Superseded&lt;/code&gt; ADRs that do not correctly reference the decision that replaced them.&lt;/p&gt;

&lt;p&gt;That may sound like a small detail, but stable validation codes make the tool especially useful for automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real benefit appears in CI
&lt;/h2&gt;

&lt;p&gt;Running ADR Guard manually is useful.&lt;/p&gt;

&lt;p&gt;Running it in the pipeline is where things become more interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate ADRs&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;adr-guard check docs/adr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, architectural documentation conventions become part of the same quality workflow already used for source code.&lt;/p&gt;

&lt;p&gt;A pull request that breaks a link between architectural decisions can fail.&lt;/p&gt;

&lt;p&gt;An incomplete ADR can fail.&lt;/p&gt;

&lt;p&gt;Two ADRs using the same identifier can fail.&lt;/p&gt;

&lt;p&gt;Of course, this does not turn architecture into code. ADR Guard cannot tell you whether Kafka is a better architectural choice than RabbitMQ.&lt;/p&gt;

&lt;p&gt;But it can verify something much more objective:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the record of that decision still structurally consistent?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That separation matters.&lt;/p&gt;

&lt;p&gt;Automate what is mechanical so that human attention can remain focused on what actually requires judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The index no longer has to be maintained manually
&lt;/h2&gt;

&lt;p&gt;ADR Guard also provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adr-guard index docs/adr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command validates the ADR set first and only then generates a deterministic Markdown index.&lt;/p&gt;

&lt;p&gt;That order is intentional.&lt;/p&gt;

&lt;p&gt;ADR Guard itself contains an ADR explaining this choice: generating an index from invalid documents could make an inconsistent set of decisions look authoritative.&lt;/p&gt;

&lt;p&gt;If validation fails, the existing index is left untouched.&lt;/p&gt;

&lt;p&gt;If nothing has changed, the file is not rewritten unnecessarily.&lt;/p&gt;

&lt;p&gt;The result may seem small, but it removes one more manual maintenance task that would otherwise eventually be forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  And then AI enters the picture with clear boundaries
&lt;/h2&gt;

&lt;p&gt;One of the more interesting capabilities added to the project is the &lt;code&gt;draft&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;ADR Guard can use OpenAI, Anthropic, Gemini, or an OpenAI-compatible endpoint to help generate an initial ADR draft.&lt;/p&gt;

&lt;p&gt;But there is an important architectural principle behind the implementation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI does not make architectural decisions on behalf of the team.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every AI-generated ADR is forced to use the &lt;code&gt;Proposed&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;ADR Guard owns the final document structure, validates the generated result, and leaves the transition to &lt;code&gt;Accepted&lt;/code&gt; under human control.&lt;/p&gt;

&lt;p&gt;That distinction is increasingly important.&lt;/p&gt;

&lt;p&gt;Using AI to accelerate documentation is very different from delegating architectural authority to a model.&lt;/p&gt;

&lt;p&gt;The project also deliberately avoids silently ingesting the entire repository.&lt;/p&gt;

&lt;p&gt;Additional context must be explicitly provided, and existing ADRs are only sent to the configured AI provider when &lt;code&gt;--include-existing-adrs&lt;/code&gt; is enabled.&lt;/p&gt;

&lt;p&gt;There is also a preview mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which allows the tool to generate, assemble, and validate the ADR without writing anything to disk.&lt;/p&gt;

&lt;p&gt;It is a sensible approach to AI in engineering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;assistance rather than unrestricted autonomy&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ADR Guard deliberately does not do
&lt;/h2&gt;

&lt;p&gt;Understanding what a tool does not do can be just as important as understanding what it does.&lt;/p&gt;

&lt;p&gt;ADR Guard does not automatically analyze a Git diff and decide that a change requires an ADR.&lt;/p&gt;

&lt;p&gt;It does not scan the entire source tree looking for architectural decisions.&lt;/p&gt;

&lt;p&gt;It does not perform RAG over the repository.&lt;/p&gt;

&lt;p&gt;It does not automatically accept architectural decisions.&lt;/p&gt;

&lt;p&gt;And it does not attempt to judge whether the recorded decision is architecturally sound.&lt;/p&gt;

&lt;p&gt;That means one responsibility remains firmly with the team:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;recognizing when a decision is significant enough to be recorded.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ADR Guard starts helping after that human decision has been made.&lt;/p&gt;

&lt;p&gt;And that is a healthy division of responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  An interesting detail: ADR Guard uses ADRs to build ADR Guard
&lt;/h2&gt;

&lt;p&gt;The project keeps its own architectural decisions under &lt;code&gt;docs/adr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Among the decisions currently documented are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using conventional Markdown ADRs;&lt;/li&gt;
&lt;li&gt;keeping the CLI free of third-party runtime dependencies;&lt;/li&gt;
&lt;li&gt;validating ADRs before generating the index;&lt;/li&gt;
&lt;li&gt;keeping AI-assisted draft generation provider-agnostic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the project applies its own rules to itself.&lt;/p&gt;

&lt;p&gt;Beyond being a good example of dogfooding, this illustrates an important point about ADRs: they are not only for giant decisions such as “monolith versus microservices.”&lt;/p&gt;

&lt;p&gt;Choosing to keep a CLI dependency-free can also be an architectural decision when that choice affects distribution, maintenance, and future evolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  ADRs should not prevent change
&lt;/h2&gt;

&lt;p&gt;This may be the most important point of all.&lt;/p&gt;

&lt;p&gt;An ADR does not exist to say:&lt;/p&gt;

&lt;p&gt;“We decided this once, so we can never change it.”&lt;/p&gt;

&lt;p&gt;Architecture has to evolve.&lt;/p&gt;

&lt;p&gt;When assumptions change, a new decision can replace the previous one. What matters is preserving the history.&lt;/p&gt;

&lt;p&gt;Instead of rewriting the past until it appears consistent with the present, create a new decision and record that it &lt;strong&gt;supersedes&lt;/strong&gt; the previous one.&lt;/p&gt;

&lt;p&gt;That sequence tells a far more useful story about the architecture than any isolated diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code records implementation; ADRs record intent and context.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A collection of ADRs creates architectural memory and reduces repeated discussions.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Even simple documentation can suffer from drift, inconsistencies, and broken relationships exactly the kind of problems automation can help prevent.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADR Guard turns ADR conventions into checks that can run locally and in CI while also keeping the decision index consistent.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI can accelerate the creation of an initial draft, but architectural decisions remain a human responsibility.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perhaps that combination is exactly what makes ADRs so useful.&lt;/p&gt;

&lt;p&gt;They are simple enough to remain human-readable documentation, but structured enough for tools to help keep them healthy.&lt;/p&gt;

&lt;p&gt;ADR Guard follows that philosophy.&lt;/p&gt;

&lt;p&gt;It is not intended to replace architects, code reviews, or technical discussions.&lt;/p&gt;

&lt;p&gt;Its goal is much more pragmatic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;once a team decides to record its architectural decisions, help ensure that this history remains readable, consistent, and trustworthy as the project grows.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;A great place to start is Michael Nygard's &lt;strong&gt;“Documenting Architecture Decisions,”&lt;/strong&gt; the article that helped popularize the modern ADR format.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Architectural Decision Records&lt;/strong&gt; community maintains a useful collection of concepts, templates, and examples, including alternatives such as MADR for teams that want slightly more detailed records.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AWS Prescriptive Guidance. Using architectural decision records to streamline technical decision-making&lt;/strong&gt; is also worth reading, especially for its discussion of ADR adoption, lifecycle, and team workflows.&lt;/p&gt;

&lt;p&gt;Another useful reference is &lt;strong&gt;The GDS Way, Documenting architecture decisions&lt;/strong&gt;, from the UK Government Digital Service, which presents a pragmatic approach to keeping architectural decisions close to the code.&lt;/p&gt;

&lt;p&gt;And, of course, you can explore &lt;strong&gt;&lt;a href="https://github.com/rodri-oliveira-dev/adr-guard" rel="noopener noreferrer"&gt;ADR Guard&lt;/a&gt;&lt;/strong&gt; itself. Beyond the implementation, the ADRs maintained inside the repository provide small real-world examples of how to document not only what was decided, but also the context and consequences behind each decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Michael Nygard — &lt;strong&gt;Documenting Architecture Decisions&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" rel="noopener noreferrer"&gt;https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Architectural Decision Records&lt;br&gt;
&lt;a href="https://adr.github.io/" rel="noopener noreferrer"&gt;https://adr.github.io/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS Prescriptive Guidance — &lt;strong&gt;Architectural Decision Records&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/welcome.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The GDS Way — &lt;strong&gt;Documenting architecture decisions&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://gds-way.digital.cabinet-office.gov.uk/standards/architecture-decisions.html" rel="noopener noreferrer"&gt;https://gds-way.digital.cabinet-office.gov.uk/standards/architecture-decisions.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ADR Guard — GitHub&lt;br&gt;
&lt;a href="https://github.com/rodri-oliveira-dev/adr-guard" rel="noopener noreferrer"&gt;https://github.com/rodri-oliveira-dev/adr-guard&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>adr</category>
      <category>documentation</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Can Big-O Complexity Be Detected at Compile Time?</title>
      <dc:creator>Rodrigo de Oliveira</dc:creator>
      <pubDate>Thu, 17 Sep 2026 20:08:02 +0000</pubDate>
      <link>https://dev.to/rodri-oliveira-dev/can-big-o-complexity-be-detected-at-compile-time-1nk1</link>
      <guid>https://dev.to/rodri-oliveira-dev/can-big-o-complexity-be-detected-at-compile-time-1nk1</guid>
      <description>&lt;p&gt;When we learn algorithm analysis, we usually practice by looking at a piece of code and asking questions such as: how many times does this loop run? Is there another loop inside it? Does this search scan an entire collection? Does this recursion split the problem in half?&lt;/p&gt;

&lt;p&gt;After a while, we start recognizing these patterns almost automatically.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;for&lt;/code&gt; loop iterating over a collection tends to suggest O(n). Two nested loops may indicate O(n²). Binary search points us toward O(log n). An efficient sorting algorithm usually lands around O(n log n).&lt;/p&gt;

&lt;p&gt;That raises an interesting question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If we can recognize these patterns by reading code, could the compiler do the same?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is exactly the question that led me to explore building a Roslyn analyzer capable of estimating algorithmic complexity at compile time.&lt;/p&gt;

&lt;p&gt;The short answer is: &lt;strong&gt;yes, to a certain extent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The more interesting answer is understanding where that “to a certain extent” begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Big-O does not measure how long a method takes
&lt;/h2&gt;

&lt;p&gt;Before talking about compilers, it is worth clarifying something important.&lt;/p&gt;

&lt;p&gt;Big-O does not measure time in milliseconds.&lt;/p&gt;

&lt;p&gt;When we say an algorithm is O(n²), we are not saying it is necessarily slow. We are describing how its computational cost grows as the input size increases.&lt;/p&gt;

&lt;p&gt;Consider:&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;for&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;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;If we assume &lt;code&gt;Process&lt;/code&gt; has constant cost, we have approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T(n) = n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add another loop:&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;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;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&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;other&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&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;In this case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T(n) = n × n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Big-O ignores constants and less relevant details so we can focus mainly on &lt;strong&gt;the rate of growth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is why it remains useful even across completely different machines.&lt;/p&gt;

&lt;p&gt;MIT, for example, introduces asymptotic complexity and recurrences early in its algorithms curriculum precisely because they provide a language for reasoning about growth without depending on specific hardware.&lt;/p&gt;

&lt;p&gt;But that abstraction also creates the first challenge for automated analysis: to infer Big-O, we need to understand not only the syntax of the code, but also &lt;strong&gt;the meaning of the operations being executed&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compiler knows much more about our code than it seems
&lt;/h2&gt;

&lt;p&gt;This is where Roslyn makes the idea interesting.&lt;/p&gt;

&lt;p&gt;The C# compiler does not see a source file as plain text.&lt;/p&gt;

&lt;p&gt;It builds a structured representation of the program called a &lt;strong&gt;Syntax Tree&lt;/strong&gt;. The syntax tree represents declarations, expressions, loops, method calls, conditionals, and practically every other language construct.&lt;/p&gt;

&lt;p&gt;That means an analyzer does not need to search for strings such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"foreach"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can ask directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is there a &lt;code&gt;ForEachStatementSyntax&lt;/code&gt; here?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More importantly, Roslyn also provides a &lt;strong&gt;Semantic Model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That difference is significant.&lt;/p&gt;

&lt;p&gt;Imagine:&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="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking only at the text, we do not know very much.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Contains&lt;/code&gt; could belong to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List&amp;lt;T&amp;gt;
HashSet&amp;lt;T&amp;gt;
Dictionary&amp;lt;TKey, TValue&amp;gt;
IEnumerable&amp;lt;T&amp;gt;
MyCustomCollection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And those operations may have very different costs.&lt;/p&gt;

&lt;p&gt;The Semantic Model can resolve the actual symbol referenced by the expression and reveal types, methods, arguments, and relationships between program elements. Roslyn's own documentation explains that the syntax tree alone is not enough to determine what an identifier actually refers to; that responsibility belongs to the semantic layer.&lt;/p&gt;

&lt;p&gt;It is precisely this combination of &lt;strong&gt;syntax and semantics&lt;/strong&gt; that makes much more sophisticated analysis possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  A seemingly innocent example
&lt;/h2&gt;

&lt;p&gt;Consider this code:&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;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;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&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;otherItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;If &lt;code&gt;otherItems&lt;/code&gt; is a &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Contains&lt;/code&gt; may scan the list looking for the element.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;items&lt;/code&gt; has &lt;code&gt;n&lt;/code&gt; elements and &lt;code&gt;otherItems&lt;/code&gt; has &lt;code&gt;m&lt;/code&gt;, we can model the cost approximately as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n × m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both collections grow at roughly the same rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now change the data structure:&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="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;otherItems&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact same line of code:&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="n"&gt;otherItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;has a very different expected algorithmic behavior.&lt;/p&gt;

&lt;p&gt;The text barely changed.&lt;/p&gt;

&lt;p&gt;The semantics changed significantly.&lt;/p&gt;

&lt;p&gt;A complexity analyzer therefore needs to resolve the symbol and recognize known operations from the libraries being used.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/rodri-oliveira-dev/complexity-analyzers" rel="noopener noreferrer"&gt;&lt;strong&gt;ComplexityAnalysis.Analyzers&lt;/strong&gt;&lt;/a&gt;, this is one of the strategies I use: known BCL and LINQ operations are identified by the &lt;strong&gt;symbol resolved by Roslyn&lt;/strong&gt;, not simply by the method name. This prevents a custom method named &lt;code&gt;Contains&lt;/code&gt; from automatically being treated as if it were &lt;code&gt;List&amp;lt;T&amp;gt;.Contains&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That detail may seem small, but it is exactly the kind of distinction that separates an interesting demo from a usable analysis tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Loops are the easy part
&lt;/h2&gt;

&lt;p&gt;Finding loops is relatively straightforward.&lt;/p&gt;

&lt;p&gt;The challenge begins when we need to understand what happens inside them.&lt;/p&gt;

&lt;p&gt;Consider:&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;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;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;What is the complexity?&lt;/p&gt;

&lt;p&gt;We do not know.&lt;/p&gt;

&lt;p&gt;If:&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="nf"&gt;Calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is O(1), then we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if &lt;code&gt;Calculate&lt;/code&gt; scans another collection of size &lt;code&gt;m&lt;/code&gt;, we may have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n × m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or perhaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;depending on the relationship between the inputs.&lt;/p&gt;

&lt;p&gt;That is why a more sophisticated analyzer needs to move beyond purely local analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Interprocedural analysis
&lt;/h2&gt;

&lt;p&gt;This is where &lt;strong&gt;interprocedural analysis&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;Imagine:&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;void&lt;/span&gt; &lt;span class="nf"&gt;Process&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&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;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;And:&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;void&lt;/span&gt; &lt;span class="nf"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;item&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="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&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;Analyzing only &lt;code&gt;Process&lt;/code&gt; is not enough.&lt;/p&gt;

&lt;p&gt;We need to follow the call to &lt;code&gt;Search&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can think of it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process
    |
    +-- loop n
            |
            +-- Search
                    |
                    +-- loop m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n × m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of analysis can reveal some interesting relationships.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;results in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loop n → B O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;results approximately in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ComplexityAnalysis.Analyzers&lt;/code&gt; performs this kind of analysis within defined limits, following reachable methods and substituting called-method parameters with caller inputs when that can be done safely.&lt;/p&gt;

&lt;p&gt;But there is a very important word here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;limits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An analyzer that runs during compilation cannot explore the application's entire call graph indefinitely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The analyzer also needs to be fast
&lt;/h2&gt;

&lt;p&gt;There is an interesting irony in all of this.&lt;/p&gt;

&lt;p&gt;It would be strange to build a performance analyzer that made compilation extremely slow.&lt;/p&gt;

&lt;p&gt;Roslyn analyzers can run while we are writing code and during the build. That means analysis cost matters. Roslyn itself supports concurrent execution of analyzer actions to improve performance, provided the analyzer has been designed to operate safely in parallel.&lt;/p&gt;

&lt;p&gt;A practical tool therefore needs to enforce analysis budgets.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maximum call depth = 5
methods analyzed per root = 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the analysis exceeds that budget, it needs to stop.&lt;/p&gt;

&lt;p&gt;That decision matters because there is a difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;building a theoretically impressive analyzer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;building an analyzer that developers are willing to keep enabled in Visual Studio.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  And then we get to recursion
&lt;/h2&gt;

&lt;p&gt;Recursion makes things even more interesting.&lt;/p&gt;

&lt;p&gt;Consider:&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;int&lt;/span&gt; &lt;span class="nf"&gt;Factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;1&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;n&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;Factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&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;We can represent its cost with a recurrence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T(n) = T(n - 1) + O(1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now consider divide and conquer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T(n) = 2T(n/2) + n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applying the Master Theorem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n log n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T(n) = 3T(n/2) + n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;results approximately in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n^1.585)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So an analyzer can go beyond simply counting loops. It can recognize certain recursive patterns, construct a recurrence, and attempt to solve it.&lt;/p&gt;

&lt;p&gt;In the project I have been developing, support is intentionally limited to known families: simple reduction, some exponential recurrences, forms compatible with the Master Theorem, and a restricted subset of Akra-Bazzi.&lt;/p&gt;

&lt;p&gt;The important word, once again, is &lt;strong&gt;restricted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because trying to solve every possible recurrence quickly takes us into very different territory.&lt;/p&gt;




&lt;h2&gt;
  
  
  So why can't we infer everything?
&lt;/h2&gt;

&lt;p&gt;This is where we reach the theoretical limit.&lt;/p&gt;

&lt;p&gt;It is tempting to imagine an analyzer that could receive any program and correctly answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This method is O(1).
This one is O(log n).
This one is O(n).
This one is O(n²).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For any arbitrary piece of code.&lt;/p&gt;

&lt;p&gt;That is not possible in general.&lt;/p&gt;

&lt;p&gt;Static program analysis runs into fundamental limits related to computability. The halting problem shows that there is no algorithm capable of correctly deciding, for every program and input, whether that program will terminate.&lt;/p&gt;

&lt;p&gt;More general results, such as Rice's theorem, show that non-trivial semantic properties of programs are undecidable in general. Cornell's program analysis material summarizes the practical consequence particularly well: static analyses need to work with &lt;strong&gt;approximations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That completely changes the question.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How can we determine the complexity of any program?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we should ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“For which structures can we produce a sufficiently safe conclusion?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more productive question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sometimes “I don't know” is the best answer
&lt;/h2&gt;

&lt;p&gt;This may be the principle I like most about this kind of tool.&lt;/p&gt;

&lt;p&gt;Consider:&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;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;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ExecuteSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;If &lt;code&gt;ExecuteSomething&lt;/code&gt; lives in an external library the analyzer knows nothing about, there are several options.&lt;/p&gt;

&lt;p&gt;It could simply assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that could be completely wrong.&lt;/p&gt;

&lt;p&gt;It could assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that would also be arbitrary.&lt;/p&gt;

&lt;p&gt;A much safer alternative is to return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I don't have enough information to state the complexity.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That may sound less impressive, but it is an important characteristic of serious analysis tools.&lt;/p&gt;

&lt;p&gt;Constant false positives destroy trust.&lt;/p&gt;

&lt;p&gt;After a while, developers start ignoring the analyzer.&lt;/p&gt;

&lt;p&gt;That is why I would rather have a tool miss some cases than invent certainty.&lt;/p&gt;

&lt;p&gt;There is an interesting parallel with static analysis in general: when a property cannot be determined precisely, we need to define how we want to approximate it. Compiler and program analysis courses deal directly with this trade-off between precision, decidability, and conservatism.&lt;/p&gt;




&lt;h2&gt;
  
  
  Big-O does not replace other metrics either
&lt;/h2&gt;

&lt;p&gt;Another important point: algorithmic complexity is not the same thing as code quality.&lt;/p&gt;

&lt;p&gt;A method can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O(1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and still be almost impossible to understand.&lt;/p&gt;

&lt;p&gt;It may contain dozens of conditionals, multiple levels of nesting, too many responsibilities, and an enormous signature.&lt;/p&gt;

&lt;p&gt;That is why it makes sense to look at different metrics independently.&lt;/p&gt;

&lt;p&gt;Cyclomatic complexity attempts to represent independent control-flow paths.&lt;/p&gt;

&lt;p&gt;Cognitive Complexity attempts to approximate the effort required to understand a given flow. SonarSource created this metric specifically to address aspects of understandability that are not well represented by cyclomatic complexity alone.&lt;/p&gt;

&lt;p&gt;We can also track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nesting depth
NLOC
statement count
parameter count
token count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these metrics replaces the others.&lt;/p&gt;

&lt;p&gt;They answer different questions.&lt;/p&gt;

&lt;p&gt;Big-O essentially asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How does cost grow with the input?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cognitive Complexity asks something closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How difficult is this flow to follow mentally?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Combining those two ideas into a single score would probably create more confusion than insight.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this becomes genuinely useful
&lt;/h2&gt;

&lt;p&gt;The goal of detecting complexity at compile time should not be to attach an academic label to every method.&lt;/p&gt;

&lt;p&gt;The real value appears when we can detect &lt;strong&gt;dangerous changes in algorithmic behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine someone writes:&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;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;customer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&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;blockedCustomers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&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="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;p&gt;During development, the dataset is small and everything runs quickly.&lt;/p&gt;

&lt;p&gt;In production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customers = 100,000
blockedCustomers = 80,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that data structure choice starts to matter.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;blockedCustomers&lt;/code&gt; is a list, the algorithm may perform an enormous number of comparisons.&lt;/p&gt;

&lt;p&gt;Choosing a more appropriate data structure can completely change the expected behavior.&lt;/p&gt;

&lt;p&gt;The analyzer does not need to prove the system's exact performance.&lt;/p&gt;

&lt;p&gt;It needs to be able to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“There is a linear operation inside an iteration that depends on input size. You may want to take a closer look.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That kind of feedback is valuable precisely because it happens &lt;strong&gt;before production, before benchmarking, and potentially even before code review&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static analysis does not eliminate benchmarks
&lt;/h2&gt;

&lt;p&gt;It is also important not to make a promise the tool cannot keep.&lt;/p&gt;

&lt;p&gt;An O(n) method may be slower than an O(n²) method for small inputs.&lt;/p&gt;

&lt;p&gt;Allocation, cache locality, I/O, branch prediction, concurrency, GC, databases, networks, JIT compilation, and many other factors influence real-world performance.&lt;/p&gt;

&lt;p&gt;Big-O is about asymptotic growth.&lt;/p&gt;

&lt;p&gt;Benchmarking is about concrete behavior under specific conditions.&lt;/p&gt;

&lt;p&gt;Tracing and profiling show what actually happened during execution.&lt;/p&gt;

&lt;p&gt;These tools complement each other.&lt;/p&gt;

&lt;p&gt;I like to think about it this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Static analysis
    ↓
Where might there be a problem?

Benchmark
    ↓
What is the actual cost?

Profiling / tracing
    ↓
Where is execution time actually being spent?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using one does not eliminate the need for the others.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compiler as an architectural feedback tool
&lt;/h2&gt;

&lt;p&gt;Exploring Big-O through Roslyn led me to a broader conclusion.&lt;/p&gt;

&lt;p&gt;Compilers do not have to exist only to transform source code into assemblies.&lt;/p&gt;

&lt;p&gt;Roslyn turns the compiler into a platform on top of which we can build tools that understand program syntax, symbols, and semantics. Microsoft itself presents this as one of the platform's core goals.&lt;/p&gt;

&lt;p&gt;That lets us move many checks closer to the moment when a decision is made.&lt;/p&gt;

&lt;p&gt;Instead of discovering certain problems in Sonar after a push, in code review a few hours later, or in production weeks later, we can provide feedback while the developer is still writing the method.&lt;/p&gt;

&lt;p&gt;That idea is particularly interesting from an architecture perspective.&lt;/p&gt;

&lt;p&gt;Whenever an architectural rule can be expressed objectively, there is a possibility of turning it into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;analyzer
architecture test
CI rule
source generator
linter
policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a shift from passive documentation to &lt;strong&gt;executable feedback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not every architectural decision can or should become an automated rule.&lt;/p&gt;

&lt;p&gt;But some certainly can.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned while exploring this idea
&lt;/h2&gt;

&lt;p&gt;The original question seemed simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can Big-O be detected at compile time?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After exploring the problem, I think a better formulation is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Which complexity properties can we infer safely enough to provide useful feedback to developers?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The difference matters.&lt;/p&gt;

&lt;p&gt;We are not trying to build an oracle.&lt;/p&gt;

&lt;p&gt;We are building a conservative analysis of a known subset of the language.&lt;/p&gt;

&lt;p&gt;Loops, known operations, LINQ, certain interprocedural calls, and some families of recurrences can produce very useful results.&lt;/p&gt;

&lt;p&gt;Dynamic code, difficult-to-resolve dispatch, unknown libraries, complex data dependencies, and structures outside the known model may remain &lt;code&gt;Unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And that is fine.&lt;/p&gt;

&lt;p&gt;In tools like this, knowing &lt;strong&gt;when not to make a claim&lt;/strong&gt; is part of the quality of the analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;Big-O can be partially inferred at compile time because Roslyn provides much more than text: we have syntax trees, symbols, types, and semantic information.&lt;/p&gt;

&lt;p&gt;The problem becomes interesting when we move beyond counting loops and start understanding operations, method calls, and recurrences.&lt;/p&gt;

&lt;p&gt;There is no algorithm, however, that can perfectly determine semantic properties like these for every arbitrary program. Practical tools need to work with known subsets and conservative approximations.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;Unknown&lt;/code&gt; result can be far better than a fabricated estimate.&lt;/p&gt;

&lt;p&gt;And perhaps most importantly, Big-O analysis does not replace benchmarks, profiling, cyclomatic complexity, or Cognitive Complexity. Each technique answers a different question.&lt;/p&gt;

&lt;p&gt;The real gain is being able to move certain performance and design signals much closer to the moment when code is being written.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;For anyone who wants to explore the topic in more depth, I would recommend a few directions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Learn — .NET Compiler Platform SDK / Roslyn APIs&lt;/strong&gt;: start with the Syntax Tree, Semantic Model, Symbols, and Diagnostic Analyzer APIs. The official documentation even includes a complete tutorial for building an analyzer and a code fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT OpenCourseWare — Introduction to Algorithms&lt;/strong&gt;: excellent material for strengthening your understanding of asymptotic analysis, recurrences, divide and conquer, and algorithm fundamentals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduction to Algorithms — Cormen, Leiserson, Rivest, and Stein (CLRS)&lt;/strong&gt;: still one of the best references for algorithms, asymptotic analysis, and recurrences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compilers: Principles, Techniques, and Tools — Aho, Lam, Sethi, and Ullman&lt;/strong&gt;: useful for understanding compilers, intermediate representations, program analysis, and optimization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cornell Program Analysis materials&lt;/strong&gt;: particularly useful for understanding data-flow analysis, conservative approximations, undecidability, and the theoretical limits of static analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive Complexity by SonarSource&lt;/strong&gt;: a useful read for understanding why algorithmic complexity, cyclomatic complexity, and understandability are related but distinct concerns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to look at a practical implementation of these ideas, &lt;a href="https://github.com/rodri-oliveira-dev/complexity-analyzers" rel="noopener noreferrer"&gt;&lt;strong&gt;ComplexityAnalysis.Analyzers&lt;/strong&gt;&lt;/a&gt; applies this reasoning with Roslyn to Big-O analysis, interprocedural calls, selected forms of recursion, and complementary complexity metrics, always preferring &lt;code&gt;Unknown&lt;/code&gt; when a safe conclusion cannot be reached.&lt;/p&gt;

&lt;p&gt;In the end, perhaps the most interesting part of this exercise is not automatically determining whether a method is O(n) or O(n²).&lt;/p&gt;

&lt;p&gt;It is realizing how much knowledge is available at compile time, and how many engineering decisions we can turn into useful feedback before code ever reaches production.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>roslyn</category>
      <category>algorithms</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
