<?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%2F87933f44-e586-43be-8232-8e13defea643.png</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>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>
