<?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: Gabriela Colombo</title>
    <description>The latest articles on DEV Community by Gabriela Colombo (@gabriela_colombo_437a7a2d).</description>
    <link>https://dev.to/gabriela_colombo_437a7a2d</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%2F2140423%2F2a02bf35-9d2a-4818-85e0-5b0aede2ad27.png</url>
      <title>DEV Community: Gabriela Colombo</title>
      <link>https://dev.to/gabriela_colombo_437a7a2d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gabriela_colombo_437a7a2d"/>
    <language>en</language>
    <item>
      <title>What I Learned Building Backend Features Used Across Multiple Clients</title>
      <dc:creator>Gabriela Colombo</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:33:33 +0000</pubDate>
      <link>https://dev.to/gabriela_colombo_437a7a2d/what-i-learned-building-backend-features-used-across-multiple-clients-3dfn</link>
      <guid>https://dev.to/gabriela_colombo_437a7a2d/what-i-learned-building-backend-features-used-across-multiple-clients-3dfn</guid>
      <description>&lt;p&gt;When engineers are early in their careers, we often think that if an API is correct, tested, and returns the expected response, the feature is ready.&lt;/p&gt;

&lt;p&gt;Working on production systems used by different clients changed this perspective.&lt;/p&gt;

&lt;p&gt;Backend changes rarely exist in isolation; the same API can be consumed by web, mobile apps, internal servers, etc. This means a backend change can be technically correct and still break the product if it’s not well planned.&lt;/p&gt;

&lt;p&gt;Over the years, I learned to think beyond the endpoint/query/mutation itself. Before any changes to an API, I like to ask, “Who consumes this data today?” “How can this be backward compatible so as not to break anything in production?”&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your API Contract Is Bigger Than the Endpoint
&lt;/h2&gt;

&lt;p&gt;An API contract isn't just the route, request body, and response documented in the specification.&lt;/p&gt;

&lt;p&gt;Clients can depend on details that may look insignificant from the backend perspective.&lt;/p&gt;

&lt;p&gt;For example, imagine an API returning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"speaker"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new requirement allows a user to have multiple roles. From a data-model perspective, changing the response to this might seem natural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"speaker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"moderator"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new model may be better, but replacing role with roles immediately creates a breaking change.&lt;/p&gt;

&lt;p&gt;The web application may already support the new model while an older mobile version still expects role.&lt;/p&gt;

&lt;p&gt;The backend therefore has to consider not only what the ideal API should look like, but how clients will transition to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Backward compatibility is part of feature development
&lt;/h2&gt;

&lt;p&gt;Deploying a breaking change to production can affect existing clients and users. For that reason, backward compatibility should be considered during development rather than after the feature is ready to be released.&lt;/p&gt;

&lt;p&gt;Depending on the situation, one approach is to introduce new data as optional first and migrate existing records before making it mandatory.&lt;/p&gt;

&lt;p&gt;One example I would like to share comes from a feature I developed around speaker roles. The new requirement was that every speaker should have a role assigned, which meant introducing information that would eventually become mandatory.&lt;/p&gt;

&lt;p&gt;Making it mandatory immediately, however, would create a problem: thousands of existing speakers had been created before this requirement existed. The new feature had to support them as well.&lt;/p&gt;

&lt;p&gt;Instead of assuming that the new requirement could be applied immediately, I treated the change as a transition.&lt;/p&gt;

&lt;p&gt;First, the new role information was introduced without requiring it for existing data. This allowed the new backend behavior to coexist with records created under the previous model.&lt;/p&gt;

&lt;p&gt;Next, I migrated the existing data. Speakers who already existed in the system needed to receive the appropriate default role so that, after the migration, both old and newly created records followed the same business rules.&lt;/p&gt;

&lt;p&gt;Only after the existing data had been migrated could the new requirement safely be treated as mandatory.&lt;/p&gt;

&lt;p&gt;Conceptually, the transition looked like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old state:&lt;/strong&gt;&lt;br&gt;
Existing speakers → no explicit role required&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transition:&lt;/strong&gt;&lt;br&gt;
New role capability → optional → existing data migrated&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New state:&lt;/strong&gt;&lt;br&gt;
All speakers → role guaranteed&lt;/p&gt;

&lt;p&gt;This is an important distinction when developing features for systems already in production. A new business rule does not automatically mean that the existing data satisfies that rule.&lt;/p&gt;

&lt;p&gt;The backend needs to provide a safe path between the old and new states.&lt;/p&gt;

&lt;p&gt;In this case, backward compatibility was not only about keeping an API response unchanged. It also meant ensuring that existing data remained valid while the product transitioned to a new requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Database redesigns need a transition state
&lt;/h2&gt;

&lt;p&gt;The database model can evolve without requiring the API contract to change in the same way.&lt;/p&gt;

&lt;p&gt;I faced this when a feature that originally supported a single role per speaker evolved to support multiple roles. The original data model was designed around a single relationship (1:1), but the new requirement needed a many-to-many relationship (1:N).&lt;/p&gt;

&lt;p&gt;Instead of replacing the old model immediately, I introduced a new relationship model and treated the change as a transition.&lt;/p&gt;

&lt;p&gt;During this transition, the API temporarily had to support both persistence models. When data was created or updated, the backend wrote the relevant information to both the legacy model and the new relationship model.&lt;/p&gt;

&lt;p&gt;Conceptually, the flow looked like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Introduce the new model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The new relationship was added while the existing model remained in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Dual write&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backend temporarily persisted the relevant information in both models. This allowed new data to remain consistent while existing data was being migrated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — Migrate existing data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Existing records were migrated to the new relationship model. During this process, both old and newly created data could continue to work without stopping the application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — Switch to the new model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the migration was complete and the new model contained the expected data, the application could rely on the new relationship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 5 — Remove the legacy path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The temporary logic that wrote to the old model was removed. Once the application no longer depended on the legacy data, the old database structure could also be safely removed.&lt;/p&gt;

&lt;p&gt;The important part was that the database change and the application change could not be treated as a single deployment.&lt;/p&gt;

&lt;p&gt;For a period of time, the backend had to understand two representations of the same business concept and keep them consistent.&lt;/p&gt;

&lt;p&gt;This approach added temporary complexity to the code, but that complexity had a purpose: it created a safe transition between two data models without requiring the production system to move from one state to another instantly.&lt;/p&gt;

&lt;p&gt;It also changed how I think about database redesigns. Sometimes the safest solution is not to replace the old model immediately, but to design an intermediate state where both models can coexist until the migration is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ”Nobody uses this field anymore” is a dangerous assumption
&lt;/h2&gt;

&lt;p&gt;Removing legacy code is an important part of evolving a system. However, assuming that something is no longer used just because we cannot find an obvious reference to it can be dangerous.&lt;/p&gt;

&lt;p&gt;Searching the codebase is a good starting point, but it doesn’t always show the whole picture. An API can have different consumers, older clients can still be running, and some code paths may only be executed under specific conditions.&lt;/p&gt;

&lt;p&gt;Observability can help answer these questions.&lt;/p&gt;

&lt;p&gt;For example, before removing an old behavior, we can add temporary logs or metrics around its usage and observe it for a period of time. Instead of assuming that a legacy path is no longer used, we can measure whether it is still being executed.&lt;/p&gt;

&lt;p&gt;A removal process can look like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identify the legacy behavior → find known consumers → instrument its usage → observe → remove → monitor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is especially useful during migrations. After moving to a new data model, I don’t want to remove the old path simply because the migration has finished. I also want to know that the application is no longer depending on it.&lt;/p&gt;

&lt;p&gt;The goal is to turn “I think nobody uses this anymore” into “we have evidence that this is no longer being used.”&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Observability after deployment
&lt;/h2&gt;

&lt;p&gt;During development, I try to think about what information I will need after the feature reaches production. This means creating consistent logs around important operations, failures, unexpected states, etc.&lt;/p&gt;

&lt;p&gt;Once the feature is deployed, these logs can be observed through tools such as Grafana or other monitoring platforms.&lt;/p&gt;

&lt;p&gt;For a migration or a gradual rollout, for example, I want to understand:&lt;/p&gt;

&lt;p&gt;if the new code path is being executed as expected&lt;br&gt;
if some records cannot be processed by the new logic&lt;br&gt;
if errors increased after the deployment&lt;br&gt;
if the system reached a state that I didn’t expect during development&lt;br&gt;
The important part is to think about observability before something goes wrong.&lt;/p&gt;

&lt;p&gt;A log such as Something went wrong might tell me that an error happened, but it gives me very little information to investigate it. Good logs should provide enough context to understand which operation failed and at which point in the flow, without exposing sensitive information.&lt;/p&gt;

&lt;p&gt;This becomes even more important when releasing changes incrementally. If old and new behaviors temporarily coexist, observability helps validate whether the transition is actually happening as expected.&lt;/p&gt;

&lt;p&gt;For me, production is the final validation environment. Tests can validate the scenarios we know about, while observability helps us discover the scenarios we didn’t anticipate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building backend features for production systems taught me that implementing the expected behavior is only part of the job.&lt;/p&gt;

&lt;p&gt;A backend engineer also needs to think about existing data, different consumers, migrations, backward compatibility, safe transitions, and how the system will be observed after deployment.&lt;/p&gt;

&lt;p&gt;The question is no longer only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;“Does the new feature work?”&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It also becomes:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;“Can the system safely move from its current state to the new one?”&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That change in perspective has influenced how I design features today. I try to think not only about the final architecture, but also about the path the system needs to take to get there.&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://medium.com/@gabriela-colombo15/what-i-learned-building-backend-features-used-across-multiple-clients-ca7e9f0fe78e" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
