<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Aggelos Bellos - TalentLMS Tech Blog</title><link>https://blog.talentlms.io/authors/bellangelo/</link><description>Posts by Aggelos Bellos - Insights and stories from the TalentLMS engineering team. Technical deep-dives, team culture, and lessons learned building learning management software.</description><language>en-US</language><managingEditor>noreply@talentlms.com (TalentLMS Engineering Team)</managingEditor><webMaster>noreply@talentlms.com (TalentLMS Engineering Team)</webMaster><lastBuildDate>Mon, 17 Nov 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.talentlms.io/authors/bellangelo/feed.xml" rel="self" type="application/rss+xml"/><image><url>https://blog.talentlms.io/images/logo/logo.svg</url><title>TalentLMS Tech Blog</title><link>https://blog.talentlms.io/</link></image><item><title>APIs as Infrastructure: Optimizing for Change</title><link>https://blog.talentlms.io/posts/apis-as-infrastructure/</link><pubDate>Mon, 17 Nov 2025 00:00:00 +0000</pubDate><dc:creator>Aggelos Bellos</dc:creator><guid>https://blog.talentlms.io/posts/apis-as-infrastructure/</guid><description>APIs need to stay stable even as the systems behind them keep changing. Each version becomes a contract that cannot move, while data models and requirements evolve around it.
Keeping everything working without slowing development is harder than it seems, and common approaches often break down once incremental changes start to accumulate.</description><enclosure url="https://blog.talentlms.io/images/posts/apis-as-infrastructure.png" type="image/png"/><media:content url="https://blog.talentlms.io/images/posts/apis-as-infrastructure.png" medium="image"><media:title type="plain">Horizontal layered strata progressing diagonally from smaller to larger forms with backward transformation arrows showing API version evolution</media:title><media:description type="plain">Horizontal layered strata progressing diagonally from smaller to larger forms with backward transformation arrows showing API version evolution</media:description></media:content><content:encoded><![CDATA[<img src="https://blog.talentlms.io/images/posts/apis-as-infrastructure.png" alt="Horizontal layered strata progressing diagonally from smaller to larger forms with backward transformation arrows showing API version evolution" style="max-width: 100%; height: auto; margin-bottom: 1.5em;" /><p style="margin-bottom: 1.5em; padding: 1em; background-color: #f5f5f5; border-left: 4px solid #0066cc;"><strong>Aggelos Bellos</strong>, Senior Software Engineer<br/>Aggelos decided to become a software engineer at a young age. At 12, he started experimenting with web technologies through a Blogspot.

He is currently working in the Architecture Team on TalentLMS, …</p><p>Managing APIs is hard. An application usually supports a single version of itself. It can be refactored, restructured, and redesigned with relative freedom. On the other hand, an API has to maintain stability for all its versions that are being consumed.</p>
<p>As requirements change, more and more time is spent on how to avoid breaking changes instead of actually delivering value.</p>
<h2 id="frozen-in-time">Frozen in Time</h2>
<p>An API is a contract between the provider and the consumer. This means that once a version is released, it should remain frozen in time.
This is not only true for the contract of the API but also for its implementation. However, this is not a &ldquo;should&rdquo; but what happens in practice.</p>
<p>When an API is released, we don&rsquo;t care about its internal implementation anymore. Yes, there will be bugs and yes, there are multiple shared components.
But other than these, we do not make changes. If it works, don&rsquo;t change it. Right?</p>
<p>Theoretically, we could develop a completely new application for each new version. This would allow us to build something and just make sure we keep it alive. Practically, the cost of maintaining such an approach is prohibitive. Furthermore, this is not how software is built in practice.</p>
<p>Software is built in small incremental batches. So why don&rsquo;t we optimize our APIs for small incremental changes?</p>
<h2 id="problem">Problem</h2>
<p>Code frozen in time sounds good, but what about code that reflects data? Data is also evolving with each new requirement.
Take this for example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> ($course<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">active</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// do something
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// do something else
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> }
</span></span></code></pre></div><p>If we delete the <code>active</code> field in favor of a new <code>status</code> field, then we will have code that depends on a field that does not exist anymore.
Even a new application wouldn&rsquo;t help us here as there is still the dependency on the data.</p>
<p>There are ways to mitigate this problem. The easiest one is to check the version of the API and adapt the code accordingly.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> ($apiVersion <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">2</span>) {
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">if</span> ($course<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;active&#39;</span>) {
</span></span><span style="display:flex;"><span>         <span style="color:#75715e">// do something
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>         <span style="color:#75715e">// do something else
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     }
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">if</span> ($course<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">active</span>) {
</span></span><span style="display:flex;"><span>         <span style="color:#75715e">// do something
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>         <span style="color:#75715e">// do something else
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     }
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>As you can see, this approach quickly becomes unmanageable. Each new version adds more complexity to the code.</p>
<p>A more common approach is to use feature flags or to split each version in a different folder / class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>  <span style="color:#f92672">-</span> <span style="color:#a6e22e">api</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">v1</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">-</span> <span style="color:#a6e22e">CourseController</span><span style="color:#f92672">.</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">-</span> <span style="color:#a6e22e">v2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">-</span> <span style="color:#a6e22e">CourseController</span><span style="color:#f92672">.</span><span style="color:#a6e22e">php</span>
</span></span></code></pre></div><p>While this can work, it does not scale well with small incremental changes. Furthermore, you tend to lose what is the latest state of the application.</p>
<h2 id="apis-as-infrastructure">APIs as Infrastructure</h2>
<p>To solve these problems, we decided to treat APIs as infrastructure. An approach first introduced by <a href="https://stripe.com/blog/api-versioning" target="_blank" rel="noopener noreferrer">Stripe</a>
 back in 2017.</p>
<p>The idea is simple. Your code always reflects the latest version of your API. Each time you need to introduce a change, you update your code to reflect the new requirements. Then, you add a <code>VersionChange</code> that lets you go back in time.</p>
<p>Instead of branching the system into multiple versions, we move the system forward and let transformations pull older versions backward. This keeps change concentrated in one place instead of fragmented across versions.</p>
<p>Let us build on our previous example. We have a <code>CourseEntity</code> that had an <code>active</code> field in <code>V1</code> and in <code>V2</code> introduced a <code>status</code> field.
We would update our code to reflect the latest version, which means that our code would use only the <code>status</code> field.</p>
<p>To make sure we won&rsquo;t break the previous versions we add a <code>VersionChange</code> that restores the <code>active</code> field from the <code>status</code> field.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ConvertCourseStatusToActiveChange</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">string</span> $description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;The active field was replaced by the status field for ...&#39;</span>;
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">apply</span>(<span style="color:#a6e22e">CourseEntity</span> $course)<span style="color:#f92672">:</span> <span style="color:#a6e22e">CourseEntity</span> {
</span></span><span style="display:flex;"><span>        $course<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">active</span> <span style="color:#f92672">=</span> $course<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;active&#39;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $course;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>When a request comes in, we check the requested version and apply all the necessary changes to bring the data to the requested version.</p>
<div class="mermaid">
  
sequenceDiagram
participant Request
participant API
participant VersionChanges
participant Response

Request->>API: Version: 2025-11-14
API->>API: Latest Version: 2025-11-17

loop Apply changes until we reach requested version
API->>VersionChanges: Apply 2025-11-16 changes
API->>VersionChanges: Apply 2025-11-15 changes
API->>VersionChanges: Apply 2025-11-14 changes
end

VersionChanges->>Response: Response from version 2025-11-14

</div>

<h3 id="heres-why-we-like-this-approach">Here&rsquo;s why we like this approach:</h3>
<ul>
<li>The code always reflects the latest version of the API.</li>
<li>Small incremental changes are easy to implement.</li>
<li>Each version change has a mandatory description that explains why the change was necessary.</li>
<li>We can freeze old versions without duplicating code.</li>
</ul>
<p>Treating APIs as infrastructure lets us evolve safely, incrementally, and without fear of breaking the past.</p>
<h2 id="keeping-versions-aligned-with-reality">Keeping Versions Aligned With Reality</h2>
<p>Most API versioning schemes assume that products evolve through major releases. Versions like <code>example.com/api/v1/courses</code> and <code>example.com/api/v2/courses</code> work well when changes arrive in large batches.</p>
<p>The problem is that major releases require coordination across departments and strict lifecycle planning. More importantly, they contradict everything we have said so far:
<code>$small_incremental_changes !== $major_release</code>.</p>
<p>Small, steady changes are easier for consumers to adopt. Ideally, the versioning scheme should reflect that and communicate something meaningful to them.</p>
<p>Date-based versioning ( YYYY-MM-DD ) does exactly that. Each version corresponds to a real point in time, making the incremental nature of our changes visible and predictable. It aligns the version history with how the API actually evolves, instead of forcing artificial release boundaries.</p>
<h2 id="design-first-change-maybe">Design First, Change Maybe</h2>
<p>Code will always remain a technical debt. Having a framework that supports change does not mean that we can avoid thinking about design. Some changes will always ripple through the system. We try to balance between over-engineering and pragmatism.</p>
<p>What matters is creating an environment where change is expected, guided, and safe. A structure that lets us introduce new behavior incrementally, without rewriting the past. An approach where old versions can be frozen with confidence, and new versions can evolve without fear.</p>
<p>By treating APIs as long-lived infrastructure rather than short-lived features, we make this balance possible. We keep the codebase aligned with the current truth of the system, we document why each version exists, and we ensure that past behavior stays accessible without forcing duplication or hacks.</p>
<h2 id="bonus">Bonus</h2>
<p>While we were experimenting with this approach, we found an open-source project that implements in FastAPI what Stripe describes in their blog post.
Having a concrete implementation really helped us to implement this approach in PHP.</p>
<p>You can check it out here: <a href="https://github.com/zmievsa/cadwyn" target="_blank" rel="noopener noreferrer">cadwyn</a>
.</p>
]]></content:encoded></item></channel></rss>