<?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: Simone Primarosa</title>
    <description>The latest articles on DEV Community by Simone Primarosa (@simonepri).</description>
    <link>https://dev.to/simonepri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2334089%2F63c2a145-7a22-4c6a-a8a5-e684a3b25232.png</url>
      <title>DEV Community: Simone Primarosa</title>
      <link>https://dev.to/simonepri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/simonepri"/>
    <language>en</language>
    <item>
      <title>Stop cross-file drift with Google's IfChange/ThenChange comments</title>
      <dc:creator>Simone Primarosa</dc:creator>
      <pubDate>Mon, 20 Apr 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/simonepri/stop-cross-file-drift-with-googles-ifchangethenchange-comments-4j5h</link>
      <guid>https://dev.to/simonepri/stop-cross-file-drift-with-googles-ifchangethenchange-comments-4j5h</guid>
      <description>&lt;p&gt;In your codebase right now, there's a comment that says &lt;em&gt;"if you change this, also update X"&lt;/em&gt;. Maybe you wrote it. Maybe your coworker did. Either way, the next person who touches the file won't read it — and at some point, something will break.&lt;/p&gt;

&lt;p&gt;This pattern is more common that one might imagine, a phrase &lt;em&gt;"keep this in sync with"&lt;/em&gt; — turns up in over &lt;a href="https://github.com/search?q=%22keep+this+in+sync+with%22&amp;amp;type=code" rel="noopener noreferrer"&gt;&lt;strong&gt;90,000&lt;/strong&gt; public code files&lt;/a&gt;, including the &lt;a href="https://github.com/search?q=%22keep+in+sync%22+repo%3Atorvalds%2Flinux&amp;amp;type=code" rel="noopener noreferrer"&gt;Linux kernel&lt;/a&gt; and &lt;a href="https://github.com/search?q=%22keep+in+sync%22+repo%3Afacebook%2Freact&amp;amp;type=code" rel="noopener noreferrer"&gt;React&lt;/a&gt;. Engineers everywhere write prose &lt;em&gt;declaring&lt;/em&gt; which files should move together; almost nowhere is anything actually &lt;em&gt;enforcing&lt;/em&gt; that declaration.&lt;/p&gt;

&lt;p&gt;Closing that gap — between "I declared the coupling" and "the build catches me when I break it" — takes two things: (1) a shared convention for marking the coupling explicitly, and (2) an enforcement layer that fails the build when someone ignores it.&lt;/p&gt;

&lt;p&gt;(1) already exists and it's called &lt;code&gt;IfThisThenThat&lt;/code&gt; — though if you haven't worked at Google (or read &lt;a href="https://filiph.net/text/ifchange-thenchange.html" rel="noopener noreferrer"&gt;this article from Filiph Hracek&lt;/a&gt;), you've probably never heard of it. Which is why this post aims at two things: giving you a primer on (1) and introducing you to a new tool that handles (2).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;IfThisThenThat&lt;/code&gt;: the pattern
&lt;/h2&gt;

&lt;p&gt;At Google there's a linter/convention called &lt;code&gt;IfThisThenThat&lt;/code&gt; — often abbreviated just as &lt;code&gt;IFTTT&lt;/code&gt;. The idea is simple, you mark two regions (across files or within one file) with a structured comment and semantically declare that changes to one require changes to the other. The linter runs at pre-submit (roughly Google's equivalent of a &lt;code&gt;pre-push&lt;/code&gt; hook) and blocks the change if one side moved and the other didn't.&lt;/p&gt;

&lt;p&gt;The linter is internal to Google, but the idea/syntax it's not. The pattern is documented publicly in &lt;a href="https://www.chromium.org/chromium-os/developer-library/guides/development/keep-files-in-sync/" rel="noopener noreferrer"&gt;Chromium's developer guide&lt;/a&gt;, and Chromium's own tree has over &lt;strong&gt;1,100&lt;/strong&gt; &lt;code&gt;LINT.IfChange&lt;/code&gt; directives in use today. TensorFlow and Fuchsia use it too.&lt;/p&gt;

&lt;p&gt;The directives look like this. Take a Kubernetes setup: your node sizes live in Terraform, your app's resource limits live in Helm, and they have to match by design — the pod has to fit the node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_container_node_pool"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;node_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# LINT.IfChange(node_size)&lt;/span&gt;
    &lt;span class="nx"&gt;machine_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"n2-standard-4"&lt;/span&gt;  &lt;span class="c1"&gt;# 4 vCPU, 16 GB&lt;/span&gt;
    &lt;span class="c1"&gt;# LINT.ThenChange(//charts/app/values.yaml:resource_limits)&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# LINT.IfChange(resource_limits)&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8Gi"&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;14Gi"&lt;/span&gt;   &lt;span class="c1"&gt;# must fit within node machine_type&lt;/span&gt;
  &lt;span class="c1"&gt;# LINT.ThenChange(//terraform/gke.tf:node_size)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Bump the Terraform node size to squeeze costs and forget the Helm side, and the linter fires:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/gke.tf:1: warning: changes in this block may need to be reflected in charts/app/values.yaml:resource_limits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Without that catch, the next deploy hits &lt;code&gt;FailedScheduling&lt;/code&gt; because the pod limits still assume the old node.&lt;/p&gt;

&lt;p&gt;You might wonder what happens if you genuinely need to change one side without the other — say, bumping the Helm &lt;code&gt;limits.memory&lt;/code&gt; from &lt;code&gt;14Gi&lt;/code&gt; to &lt;code&gt;15Gi&lt;/code&gt; when the value still fits inside the existing node capacity. For those, &lt;code&gt;NO_IFTTT=&amp;lt;reason&amp;gt;&lt;/code&gt; in the commit message tells the linter to skip the check.&lt;/p&gt;

&lt;p&gt;That's the whole mechanism. No AST magic, no schema system, no attempt to infer architecture. Two comments, one rule, one escape-hatch: if this block changes, that one must too — unless you say otherwise.&lt;/p&gt;

&lt;p&gt;The only problem? &lt;strong&gt;Google has never released the linter behind the pattern to the public.&lt;/strong&gt; Which stands out — Google is otherwise known for open-sourcing similar internal tools (like &lt;a href="https://github.com/google/keep-sorted" rel="noopener noreferrer"&gt;&lt;code&gt;keep-sorted&lt;/code&gt;&lt;/a&gt;); this one just stayed inside.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;ifttt-lint&lt;/code&gt;: the enforcement
&lt;/h2&gt;

&lt;p&gt;I'm a strong believer that conventions without enforcement aren't useful. That's why I built &lt;a href="https://github.com/simonepri/ifttt-lint" rel="noopener noreferrer"&gt;&lt;code&gt;ifttt-lint&lt;/code&gt;&lt;/a&gt; — an open-source Rust reimplementation of Google's internal linter, with the same directive syntax and semantics.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&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/simonepri" rel="noopener noreferrer"&gt;
        simonepri
      &lt;/a&gt; / &lt;a href="https://github.com/simonepri/ifttt-lint" rel="noopener noreferrer"&gt;
        ifttt-lint
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🔗 Stop cross-file drift with Google's IfChange/ThenChange comments
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a href="https://github.com/simonepri/ifttt-lint" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fsimonepri%2Fifttt-lint%2FHEAD%2Fassets%2Fbanner.png" alt="ifttt-lint — stop cross-file drift with Google's IfChange/ThenChange comments" width="600"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;strong&gt;Stop cross-file drift with Google's &lt;code&gt;IfChange&lt;/code&gt; / &lt;code&gt;ThenChange&lt;/code&gt; comments.&lt;/strong&gt;
  &lt;br&gt;
  
    Open-source reimplementation of &lt;a href="https://www.chromium.org/chromium-os/developer-library/guides/development/keep-files-in-sync/" rel="nofollow noopener noreferrer"&gt;Google's internal IfThisThenThat linter&lt;/a&gt;
  
&lt;/p&gt;
&lt;p&gt;
  &lt;a href="https://crates.io/crates/ifttt-lint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f5207f4599c365627a9053001fc5f741fb60cd537dd7cde23436fc5fea985ac3/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f762f69667474742d6c696e742e737667" alt="crates.io"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/simonepri/ifttt-lint/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1ea6fa995d98c32cd11518e851a6a2ec2ca352dd88d57d011722d86af44f340d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f73696d6f6e657072692f69667474742d6c696e742e737667" alt="license"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;You add a field to a Go struct and forget the TypeScript mirror. You bump a constant and forget the docs. You rename a database column and forget the migration. You only discover it when something breaks in production — or worse, when a user reports it weeks later.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ifttt-lint&lt;/code&gt; is built to catch exactly that. You wrap co-dependent sections in &lt;code&gt;LINT.IfChange&lt;/code&gt; / &lt;code&gt;LINT.ThenChange&lt;/code&gt; comment directives. When a diff touches one side but not the other, the tool fails — before the change reaches production. The model is intentionally simple, which keeps it predictable.&lt;/p&gt;

&lt;p&gt;This repo dogfoods its own directives to keep the tool version in sync across &lt;a href="https://github.com/simonepri/ifttt-lint/Cargo.toml" rel="noopener noreferrer"&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/a&gt;, the &lt;a href="https://github.com/simonepri/ifttt-lint#pre-commit-recommended" rel="noopener noreferrer"&gt;pre-commit config&lt;/a&gt;, and the &lt;a href="https://github.com/simonepri/ifttt-lint/.github/workflows/ci-cd.yml" rel="noopener noreferrer"&gt;CI release pipeline&lt;/a&gt;. Automation (&lt;a href="https://github.com/release-plz/release-plz" rel="noopener noreferrer"&gt;release-plz&lt;/a&gt;) does the normal sync; the directives catch…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/simonepri/ifttt-lint" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Install it as a GitHub Action, a &lt;code&gt;pre-commit&lt;/code&gt; hook, or via &lt;code&gt;cargo install ifttt-lint&lt;/code&gt;, and you can start using the pattern in your repo today. Copy-pasteable configs live in the &lt;a href="https://github.com/simonepri/ifttt-lint#setup" rel="noopener noreferrer"&gt;README's setup section&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're worried about performance, don't be — &lt;code&gt;ifttt-lint&lt;/code&gt; validates &lt;a href="https://github.com/simonepri/ifttt-lint#performance" rel="noopener noreferrer"&gt;Chromium's 488k-file tree in under a second&lt;/a&gt; on an M-series MacBook, which makes it comfortable as a &lt;code&gt;pre-commit&lt;/code&gt; hook on any realistic codebase you'll ever touch. Forty-plus languages are supported out of the box — C, C++, Go, Rust, Java, Kotlin, Swift, TypeScript, Python, Ruby, Shell, YAML, Terraform, SQL, Markdown, and the rest of the usual suspects — and unknown extensions fall back to &lt;code&gt;//&lt;/code&gt; / &lt;code&gt;/*&lt;/code&gt; / &lt;code&gt;#&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Under the hood it runs three validation passes in a single go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diff-based&lt;/strong&gt; — &lt;em&gt;"you touched the &lt;code&gt;IfChange&lt;/code&gt; block but not the &lt;code&gt;ThenChange&lt;/code&gt; target."&lt;/em&gt; The core check; this is what runs on every PR, and it's the one &lt;code&gt;NO_IFTTT=&amp;lt;reason&amp;gt;&lt;/code&gt; skips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural&lt;/strong&gt; — &lt;em&gt;"your &lt;code&gt;ThenChange&lt;/code&gt; points at a file or label that doesn't exist."&lt;/em&gt; Catches typos, renames, and dead references at commit time, before a reviewer has to. Always runs, even when &lt;code&gt;NO_IFTTT&lt;/code&gt; is set — you can't accidentally ship a reference to a file that doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse-lookup&lt;/strong&gt; — &lt;em&gt;"you renamed a label and left surviving stale references elsewhere."&lt;/em&gt; Without this pass, stale references quietly accumulate and the directives themselves turn into the maintenance burden they were meant to prevent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My intuition tells me this pattern will become more valuable, not less, as more code gets written by AI. Coding agents routinely introduce cross-file couplings while generating code — a constant duplicated in two places, a struct that mirrors a DB row, a value the agent decided should live in both code &lt;em&gt;and&lt;/em&gt; config — and the human operator often doesn't notice. Drop the &lt;a href="https://github.com/simonepri/ifttt-lint#using-with-coding-agents" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; snippet from the README&lt;/a&gt; into your repo, and your coding agent will add &lt;code&gt;LINT.IfChange&lt;/code&gt; directives whenever it produces new co-dependent code. The linter then catches the next change that breaks them, regardless of who (or what) made it.&lt;/p&gt;

&lt;p&gt;Are there any gotchas? One worth naming: code moves aren't tracked semantically. If you move a block from one file to another &lt;em&gt;and&lt;/em&gt; change its content in the same commit, &lt;code&gt;ifttt-lint&lt;/code&gt; sees a delete and an add — not "moved and modified" — so &lt;code&gt;ThenChange&lt;/code&gt; targets aren't re-evaluated against the new location. When this matters, prefer splitting the change in two: the move first, the content edit after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait — isn't this just papering over bad code?
&lt;/h2&gt;

&lt;p&gt;Fair question. Generally counter-arguments against the existence of this pattern are right: a shared schema (Protocol Buffers, Thrift, GraphQL) really does eliminate cross-language drift when you can use one. Codegen really does make the coupling disappear when a single generator owns both sides. Types and constants really do solve the problem within a single language. When any of these apply, reach for them first — not for &lt;code&gt;LINT.IfChange&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The catch is practical, not philosophical. Replatforming a Terraform ↔ Helm boundary onto cdk8s or Pulumi is weeks/months of work measured against two comment directives. Introducing a schema registry for one cross-language type shared between two services is a new piece of infrastructure to own. And some couplings — a constant and the English sentence describing it in the docs, a Prometheus alert threshold and the rate limit in application code, an encoder and its decoder, a migration and the struct that reads from it — don't have a clean "proper" fix at all; they're structural by design.&lt;/p&gt;

&lt;p&gt;Those &lt;a href="https://github.com/search?q=%22keep+in+sync%22&amp;amp;type=code" rel="noopener noreferrer"&gt;19,000+ &lt;em&gt;"keep in sync"&lt;/em&gt; comments&lt;/a&gt; scattered across open source are evidence that engineers already know when the pure fix isn't worth it; they just don't have anything backing the intent. The linter is the missing half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go try it out!
&lt;/h2&gt;

&lt;p&gt;If you've read this far, you've probably already thought of at least one place in your own code that could use a directive. And — in the spirit of the pattern itself — &lt;em&gt;if this&lt;/em&gt; tool looks useful, &lt;strong&gt;then&lt;/strong&gt; ⭐ &lt;a href="https://github.com/simonepri/ifttt-lint" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;. It's the fastest signal that this kind of tool is worth building/maintaining.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;I no longer work at Google. &lt;code&gt;ifttt-lint&lt;/code&gt; is an independent project, not affiliated with or endorsed by Google in any way, and shares no code with the internal version. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>opensource</category>
      <category>productivity</category>
      <category>tooling</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
