<?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>Forem: Debdut Chakraborty</title>
    <description>The latest articles on Forem by Debdut Chakraborty (@debdut).</description>
    <link>https://forem.com/debdut</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%2F537476%2Fbe748dc5-aa19-48b0-b5c9-e2cc51c506dd.jpg</url>
      <title>Forem: Debdut Chakraborty</title>
      <link>https://forem.com/debdut</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/debdut"/>
    <language>en</language>
    <item>
      <title>Beginners' guide to Go Contexts: The Magic Controller of Goroutines</title>
      <dc:creator>Debdut Chakraborty</dc:creator>
      <pubDate>Fri, 06 Mar 2026 21:17:15 +0000</pubDate>
      <link>https://forem.com/rocketchat/beginners-guide-to-go-contexts-the-magic-controller-of-goroutines-158c</link>
      <guid>https://forem.com/rocketchat/beginners-guide-to-go-contexts-the-magic-controller-of-goroutines-158c</guid>
      <description>&lt;p&gt;We've all used contexts, usually by passing them to functions that require them, like HTTP handlers or database queries. But what exactly are contexts, and how do they work under the hood?&lt;/p&gt;

&lt;p&gt;In Go, a Context is essentially a signal. It travels through your functions to tell them when they should stop working because the data is no longer needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basic Check
&lt;/h2&gt;

&lt;p&gt;The most fundamental way to use a context is to check its state manually. This is perfect for long-running loops or heavy calculations. If a function is a "one-off" and finishes instantly, a context doesn't add much value.&lt;/p&gt;

&lt;p&gt;However, for a loop like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="c"&gt;// check if the signal says we should stop  &lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stopping early:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="c"&gt;// simulate some work  &lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&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 didn't have that if &lt;code&gt;err := ctx.Err()&lt;/code&gt; check, the goroutine would keep spinning even if the user who started it has already disconnected or timed out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Powering up with Select
&lt;/h2&gt;

&lt;p&gt;While checking &lt;code&gt;ctx.Err()&lt;/code&gt; works for loops, the real magic happens with the &lt;code&gt;select&lt;/code&gt; statement. This is how you make a goroutine "listen" for a cancellation signal while it is busy doing something else, like waiting for a channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Waiting for a result
&lt;/h3&gt;

&lt;p&gt;Imagine you are fetching data from a slow API. You want the data, but you aren't willing to wait forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;resultCh&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// simulate a slow task  &lt;/span&gt;
        &lt;span class="n"&gt;resultCh&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="s"&gt;"got the data!"&lt;/span&gt;  
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;resultCh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"received:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
        &lt;span class="c"&gt;// ctx.Done() is a channel that closes when the context is cancelled  &lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gave up waiting:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;By using select, your code becomes responsive. The moment the context expires, the &lt;code&gt;&amp;lt;-ctx.Done()&lt;/code&gt; case triggers, and your function can exit immediately instead of hanging for the full 5 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layered Control
&lt;/h2&gt;

&lt;p&gt;Contexts are designed to be passed down. If you create a "child" context from a "parent," and the parent is cancelled, all the children are cancelled too. This lets you stop an entire tree of goroutines from one single place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="c"&gt;// create a child context we can cancel manually  &lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// this starts the loop from earlier&lt;/span&gt;

    &lt;span class="c"&gt;// simulate another part of the app failing  &lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"something else failed!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// this kills the 'process' goroutine too  &lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Making Existing Code Context-Aware
&lt;/h2&gt;

&lt;p&gt;You might have a library or an old function that doesn't support contexts yet. How do you "wrap" it so it respects a timeout?&lt;/p&gt;

&lt;p&gt;The trick is to run the old code in a separate goroutine and use a select statement to wait for either the result or the context signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ContextAwareWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;resultCh&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;string&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="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="c"&gt;// call the old, non-context-aware function  &lt;/span&gt;
        &lt;span class="n"&gt;resultCh&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;OldLegacyFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
        &lt;span class="c"&gt;// if the context expires first, we return an error  &lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;resultCh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  
        &lt;span class="c"&gt;// if the work finishes first, we return the result  &lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;blockquote&gt;
&lt;p&gt;Note: Using a &lt;strong&gt;buffered channel&lt;/strong&gt; (&lt;code&gt;make(chan string, 1)&lt;/code&gt;) is important here. It ensures that if the context times out and we exit the function, the goroutine still running the OldLegacyFunction can send its result to the channel and exit without getting stuck forever (a goroutine leak).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Importance of Cancel and Defer
&lt;/h2&gt;

&lt;p&gt;Whenever you use &lt;code&gt;context.WithCancel&lt;/code&gt;, &lt;code&gt;WithTimeout&lt;/code&gt;, or &lt;code&gt;WithDeadline&lt;/code&gt;, the standard library gives you back a new &lt;code&gt;context&lt;/code&gt; and a &lt;code&gt;cancel&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You must call that cancel function.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even if your function finishes successfully, you should call it. The best way to do this is with &lt;code&gt;defer&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="c"&gt;// this ensures that when main finishes, the context is cleaned up  &lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 

    &lt;span class="n"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;Why is this important?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Cleanup:&lt;/strong&gt; Behind the scenes, the parent context keeps track of its children. If you don't call &lt;code&gt;cancel&lt;/code&gt;, the parent might keep a reference to the child in memory until the parent itself dies, leading to a memory leak.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop Ongoing Work:&lt;/strong&gt; Calling &lt;code&gt;cancel()&lt;/code&gt; sends the signal through the &lt;code&gt;ctx.Done()&lt;/code&gt; channel. It tells every function using that context: "The party is over, stop whatever you are doing."&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>go</category>
      <category>developer</category>
      <category>programming</category>
    </item>
    <item>
      <title>Conditions, Phases, and Declarative Phase Rules in Kubernetes Operators</title>
      <dc:creator>Debdut Chakraborty</dc:creator>
      <pubDate>Fri, 13 Feb 2026 22:40:31 +0000</pubDate>
      <link>https://forem.com/rocketchat/conditions-phases-and-declarative-phase-rules-in-kubernetes-operators-l2h</link>
      <guid>https://forem.com/rocketchat/conditions-phases-and-declarative-phase-rules-in-kubernetes-operators-l2h</guid>
      <description>&lt;h2&gt;
  
  
  Tl;Dr;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You can start with experimenting with the demo: &lt;a href="https://debdutdeb.github.io/kubernetes-phase-rules/" rel="noopener noreferrer"&gt;https://debdutdeb.github.io/kubernetes-phase-rules/&lt;/a&gt;, linking conditions with phases. It's fun. At least was to me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec&lt;/strong&gt; = desired state; &lt;strong&gt;status&lt;/strong&gt; = observed state. Controllers write status; the &lt;a href="https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status" rel="noopener noreferrer"&gt;API conventions&lt;/a&gt; describe this split and the role of conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditions&lt;/strong&gt; are the right primitive: one observation per condition type, standardized and tooling-friendly. We use &lt;em&gt;specific&lt;/em&gt; condition types so each observable fact is explicit and consumable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase&lt;/strong&gt; is still useful as a single, high-level label for observability (UIs, alerts, filters), but it should be &lt;em&gt;derived&lt;/em&gt; from conditions, not maintained as a separate state machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase rules&lt;/strong&gt; declare “when these conditions hold, phase is X.” The first matching rule wins. That keeps a single source of truth (conditions), makes phase logic testable and explicit, and avoids duplication across consumers.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;kubernetes-phase-rules&lt;/strong&gt; (&lt;a href="https://github.com/debdutdeb/kubernetes-phase-rules" rel="noopener noreferrer"&gt;https://github.com/debdutdeb/kubernetes-phase-rules&lt;/a&gt;) package provides the rule types, matchers, and a StatusManager that keeps conditions and phase in sync and patches status for you.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  The problem we faced
&lt;/h2&gt;

&lt;p&gt;At Rocket.Chat we have an operator called Airlock that originally started as an operator for mongodb user management. But recently expanding to help us manage a few more aspects of our database operations.&lt;/p&gt;

&lt;p&gt;We added a custom resource, Backup, whose status depended on several independent facts: Is the backup store available? Is database access granted? Has the backup job been scheduled? Did it complete or fail? Each of those is a separate observation, and in practice they’re often discovered or updated at different times, sometimes by different parts of the same controller or even by different controllers. So we ended up with &lt;strong&gt;multiple conditions&lt;/strong&gt; on one resource: &lt;code&gt;BucketStoreReady&lt;/code&gt;, &lt;code&gt;MongoDBAccessRequestReady&lt;/code&gt;, &lt;code&gt;JobScheduled&lt;/code&gt;, &lt;code&gt;JobCompleted&lt;/code&gt;, &lt;code&gt;JobFailed&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;We also needed &lt;strong&gt;one phase&lt;/strong&gt;—a single label like &lt;code&gt;Pending&lt;/code&gt;, &lt;code&gt;Running&lt;/code&gt;, &lt;code&gt;Completed&lt;/code&gt;, or &lt;code&gt;Failed&lt;/code&gt;—for UIs, alerts, and runbooks. That phase had to reflect the &lt;em&gt;combination&lt;/em&gt; of all those conditions: “Failed if the store is missing or the job failed; Running if the store and access are ready and the job is scheduled; Completed if the job completed,” etc. The hard part was &lt;strong&gt;managing that mapping&lt;/strong&gt; as we scaled out: multiple controllers or reconciliation steps each setting a subset of conditions, and every consumer (and the controller itself) needing a consistent answer to “what phase is this?” without reimplementing the same condition→phase logic in multiple places. We wanted a single source of truth (the conditions), one place that defined how conditions map to phase, and no drift between what the controller thinks the phase is and what dashboards or alerts assume.&lt;/p&gt;

&lt;p&gt;This is about why &lt;strong&gt;conditions&lt;/strong&gt; are the right primitive, why we still want a &lt;strong&gt;phase&lt;/strong&gt;, and how &lt;strong&gt;phase rules&lt;/strong&gt; let us derive phase from conditions in one place and keep everything in sync, including when multiple controllers touch the same resource.&lt;/p&gt;

&lt;p&gt;The post will consistently refer to airlock and the backup resource as examples. It's easier that way for my memory to keep track of things.&lt;/p&gt;




&lt;h2&gt;
  
  
  Spec and status: desired vs observed
&lt;/h2&gt;

&lt;p&gt;In the Kubernetes API, every resource that has mutable state is split into two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;spec&lt;/code&gt;&lt;/strong&gt; — The &lt;em&gt;desired&lt;/em&gt; state: what the user or automation asked for. It is the source of truth for “what should be true.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status&lt;/code&gt;&lt;/strong&gt; — The &lt;em&gt;observed&lt;/em&gt; state: what the system has actually observed. Controllers write here; users typically don’t. It answers “what is true right now.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status" rel="noopener noreferrer"&gt;Kubernetes API conventions&lt;/a&gt; spell this out clearly: the specification is a complete description of the desired state and is persisted with the object; the status summarizes the current state and is “usually persisted with the object by automated processes.” So when you build a controller, you read &lt;code&gt;spec&lt;/code&gt;, do work, and write what you observed into &lt;code&gt;status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That separation matters. It keeps user intent (spec) from being overwritten by controller updates, allows different access control for spec vs status, and gives clients a stable place to read “what’s actually happening” without parsing controller logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why conditions (and why specific ones)
&lt;/h2&gt;

&lt;p&gt;The standard way to put “what’s actually happening” into status is &lt;strong&gt;conditions&lt;/strong&gt;. A condition is a single observation: a &lt;strong&gt;type&lt;/strong&gt; (e.g. &lt;code&gt;Ready&lt;/code&gt;, &lt;code&gt;JobCompleted&lt;/code&gt;, &lt;code&gt;BucketStoreReady&lt;/code&gt;), a &lt;strong&gt;status&lt;/strong&gt; (&lt;code&gt;True&lt;/code&gt;, &lt;code&gt;False&lt;/code&gt;, or &lt;code&gt;Unknown&lt;/code&gt;), and usually a &lt;strong&gt;reason&lt;/strong&gt; and &lt;strong&gt;message&lt;/strong&gt;. The API conventions describe conditions as &lt;a href="https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties" rel="noopener noreferrer"&gt;“a standard mechanism for higher-level status reporting”&lt;/a&gt;: they let tools and other controllers understand resource state without implementing your controller’s logic. Conditions should “complement more detailed information” in status; they’re the contract for “is this thing ready / failed / still working?”&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;why specific condition types?&lt;/strong&gt; Because each condition should represent &lt;em&gt;one&lt;/em&gt; observable fact. If you only had a single “Status” condition, you’d lose information: you couldn’t tell “store not ready” from “job failed” from “job still running.” By defining conditions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BucketStoreReady&lt;/code&gt; — Can we see and use the backup store?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MongoDBAccessRequestReady&lt;/code&gt; — Is database access granted?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JobScheduled&lt;/code&gt; — Has the backup job been scheduled?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JobCompleted&lt;/code&gt; — Did the job finish successfully?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JobFailed&lt;/code&gt; — Did the job fail?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you give the controller a place to report each fact as it learns it, and you give dashboards, alerts, and other controllers a way to react to specific causes (e.g. “alert when &lt;code&gt;JobFailed&lt;/code&gt; is True” or “show message when &lt;code&gt;BucketStoreReady&lt;/code&gt; is False”).&lt;/p&gt;

&lt;p&gt;The conventions also say: condition type names should describe the &lt;em&gt;current observed state&lt;/em&gt; (adjectives or past-tense verbs like “Ready”, “Succeeded”, “Failed”), and the &lt;em&gt;absence&lt;/em&gt; of a condition should be treated like &lt;code&gt;Unknown&lt;/code&gt;. So you design conditions to be small, explicit observations; the controller sets them as it reconciles; and the rest of the system consumes them without reimplementing your state machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why phase when we already have conditions?
&lt;/h2&gt;

&lt;p&gt;Conditions are the right &lt;em&gt;primitive&lt;/em&gt;: they’re granular, extensible, and standardized. But they’re also &lt;em&gt;many&lt;/em&gt;. For a Backup or any other resource for that matter, you might have five or six conditions. For a user or a dashboard, the first question is often: “What state is this in? Running? Failed? Pending?” Answering that from raw conditions means “run the same logic the controller would use to decide the high-level state” — and that logic then lives in every consumer (CLI, UI, Prometheus, runbooks). That duplicates logic and drifts over time.&lt;/p&gt;

&lt;p&gt;A familiar example is the &lt;strong&gt;Node&lt;/strong&gt; in Kubernetes. The node controller and kubelet set several &lt;strong&gt;conditions&lt;/strong&gt; on a Node (e.g. &lt;code&gt;Ready&lt;/code&gt;, &lt;code&gt;DiskPressure&lt;/code&gt;, &lt;code&gt;MemoryPressure&lt;/code&gt;, &lt;code&gt;NetworkUnavailable&lt;/code&gt;). Those conditions drive behavior: the scheduler uses them to decide where to place pods; taints and other node properties can be updated based on conditions. But for “is this node usable?” you need a single picture. The Node’s &lt;strong&gt;phase&lt;/strong&gt; (e.g. &lt;code&gt;Running&lt;/code&gt;) is that summary, the final, high-level state that users and automation care about. So conditions are the levers the system uses to change node properties and make decisions; phase is the outcome you read when you want to know the node’s overall state.&lt;/p&gt;

&lt;p&gt;So we still want a &lt;strong&gt;phase&lt;/strong&gt;: a single, high-level label like &lt;code&gt;Pending&lt;/code&gt;, &lt;code&gt;Running&lt;/code&gt;, &lt;code&gt;Completed&lt;/code&gt;, or &lt;code&gt;Failed&lt;/code&gt; that means “the outcome of applying our rules to the current conditions.” Phase is the &lt;strong&gt;observability contract&lt;/strong&gt;: one field that UIs can show in a column, that alerting can filter on (“alert if phase != Ready”), and that runbooks can branch on, without each consumer reimplementing the condition→state rules.&lt;/p&gt;

&lt;p&gt;The Kubernetes API conventions actually &lt;a href="https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties" rel="noopener noreferrer"&gt;deprecate the old use of &lt;code&gt;phase&lt;/code&gt;&lt;/a&gt; as a first-class state-machine enum in core resources, because adding new enum values breaks compatibility and phase was often used instead of explicit conditions. The better pattern is: &lt;strong&gt;conditions are the source of truth; phase is a derived summary&lt;/strong&gt;. So we keep conditions as the only thing the controller writes, and we &lt;em&gt;compute&lt;/em&gt; phase from those conditions using a clear, declarative set of rules. That way we get the observability benefit of a single phase field without turning phase into an independent state machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The phase rule idea
&lt;/h2&gt;

&lt;p&gt;Instead of the controller imperatively setting phase in code (“if store missing then phase = Failed”), we &lt;strong&gt;declare&lt;/strong&gt; rules: “phase is &lt;code&gt;Failed&lt;/code&gt; when condition A is True or B is True; phase is &lt;code&gt;Running&lt;/code&gt; when C and D are True and E is False; …”. The relationship is one-way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The controller only updates &lt;strong&gt;conditions&lt;/strong&gt; (e.g. via &lt;code&gt;SetCondition&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase&lt;/strong&gt; is derived by evaluating an ordered list of &lt;strong&gt;phase rules&lt;/strong&gt; over the current conditions.&lt;/li&gt;
&lt;li&gt;The first rule whose condition matcher matches the current conditions gives the phase; if none match, phase is &lt;code&gt;Unknown&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So conditions drive phase, phase never drives conditions. The same condition set always produces the same phase for a given rule list. Rule order encodes priority (e.g. “Completed” before “Running” before “Failed” before “Pending”). Because phase is always recomputed from the &lt;em&gt;current&lt;/em&gt; conditions, it doesn’t matter which controller or which reconciliation step last wrote a condition—whoever updates conditions, the same rules apply and phase stays consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example (conceptually):&lt;/strong&gt; For a Backup custom resource:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phase &lt;strong&gt;Completed&lt;/strong&gt; when: &lt;code&gt;BucketStoreReady=True&lt;/code&gt;, &lt;code&gt;MongoDBAccessRequestReady=True&lt;/code&gt;, &lt;code&gt;JobCompleted=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Phase &lt;strong&gt;Running&lt;/strong&gt; when: store and access are True, &lt;code&gt;JobScheduled=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Phase &lt;strong&gt;Failed&lt;/strong&gt; when: store or access is False, or &lt;code&gt;JobFailed=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Phase &lt;strong&gt;Pending&lt;/strong&gt; when: store or access is Unknown, or job not yet scheduled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a &lt;strong&gt;phase rule&lt;/strong&gt;: a phase name plus a matcher over conditions. You evaluate the list in order; the first match wins. That’s the phase rule idea: declarative, testable, and a single place to define “what phase means.”&lt;/p&gt;




&lt;h2&gt;
  
  
  The kubernetes-phase-rules package
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/debdutdeb/kubernetes-phase-rules" rel="noopener noreferrer"&gt;kubernetes-phase-rules&lt;/a&gt; module provides exactly that: a small, &lt;strong&gt;experimental&lt;/strong&gt; and &lt;strong&gt;potentially incomplete&lt;/strong&gt; Go library for defining phase rules and computing phase from &lt;code&gt;[]metav1.Condition&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The core type is the &lt;strong&gt;&lt;code&gt;PhaseRule&lt;/code&gt;&lt;/strong&gt; interface. A phase rule has three methods: &lt;strong&gt;&lt;code&gt;Satisfies(conditions)&lt;/code&gt;&lt;/strong&gt; returns true if the given slice of &lt;code&gt;metav1.Condition&lt;/code&gt; matches the rule (e.g. all required conditions present with the right statuses for an AND rule, or at least one for an OR rule); &lt;strong&gt;&lt;code&gt;Phase()&lt;/code&gt;&lt;/strong&gt; returns the phase name this rule represents (e.g. &lt;code&gt;"Running"&lt;/code&gt;, &lt;code&gt;"Failed"&lt;/code&gt;); &lt;strong&gt;&lt;code&gt;ComputePhase(conditions)&lt;/code&gt;&lt;/strong&gt; returns that phase name when the rule is satisfied, and the constant &lt;strong&gt;&lt;code&gt;PhaseUnknown&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;"Unknown"&lt;/code&gt;) otherwise. So a phase rule is “when these conditions hold, the phase is X”; you build concrete rules with &lt;code&gt;NewPhaseRule(phaseName, matcher)&lt;/code&gt; and pass them to the StatusManager or evaluate them yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PhaseRule&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Satisfies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;metav1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ComputePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;metav1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package &lt;code&gt;rules&lt;/code&gt;&lt;/strong&gt; — You build &lt;strong&gt;phase rules&lt;/strong&gt; with &lt;code&gt;NewPhaseRule(phaseName, matcher)&lt;/code&gt;. Matchers are built from:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ConditionEquals(conditionType, statuses...)&lt;/code&gt; — this condition type must have one of the given statuses (&lt;code&gt;True&lt;/code&gt;, &lt;code&gt;False&lt;/code&gt;, &lt;code&gt;Unknown&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ConditionsAll(...)&lt;/code&gt; — all of the given condition matchers must match (logical AND).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ConditionsAny(...)&lt;/code&gt; — at least one must match (logical OR).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Given a slice of &lt;code&gt;metav1.Condition&lt;/code&gt;, you call &lt;code&gt;rule.Satisfies(conditions)&lt;/code&gt; or &lt;code&gt;rule.ComputePhase(conditions)&lt;/code&gt;; for a list of rules, you iterate in order and take the first satisfied rule’s phase (or &lt;code&gt;PhaseUnknown&lt;/code&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package &lt;code&gt;conditions&lt;/code&gt;&lt;/strong&gt; — A &lt;strong&gt;StatusManager&lt;/strong&gt; ties this into controller-runtime: you give it the CR’s status conditions pointer, the CR (implementing a small interface with &lt;code&gt;SetPhase&lt;/code&gt; / &lt;code&gt;GetPhase&lt;/code&gt; / &lt;code&gt;SetObservedGeneration&lt;/code&gt;), and the phase rules. When you call &lt;code&gt;SetCondition&lt;/code&gt; or &lt;code&gt;SetConditions&lt;/code&gt;, it updates the condition slice, recomputes phase from the rules, updates the object’s phase and observed generation, and patches status via &lt;code&gt;client.Status().Patch&lt;/code&gt; only when something changed. So the controller only sets conditions; the manager keeps phase and status in sync.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Yes, I ran out of name ideas.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Object2&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SetPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;GetPhase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;SetObservedGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generation&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module is &lt;strong&gt;experimental&lt;/strong&gt; and kept intentionally simple: minimal API, minimal dependencies, no feature creep. You can use it as a starting point and adapt the rules or the manager to your CRDs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it in the browser
&lt;/h2&gt;

&lt;p&gt;You can see phase rules in action and experiment with conditions and rule order in the interactive demo, load the templates of build your own rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo (GitHub Pages):&lt;/strong&gt; &lt;a href="https://debdutdeb.github.io/kubernetes-phase-rules" rel="noopener noreferrer"&gt;https://debdutdeb.github.io/kubernetes-phase-rules/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo lets you define condition types, set their statuses, and define an ordered list of phase rules (with AND/OR and allowed statuses). It computes the resulting phase so you can build intuition for how conditions map to phase and why rule order matters.&lt;/p&gt;




</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>opensource</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
