<?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%2F7e767032-8a6f-437a-ae55-faea5baf5eda.jpg</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>How I Migrated a Live Database to a Many-to-Many Relationship</title>
      <dc:creator>Gabriela Colombo</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:50:21 +0000</pubDate>
      <link>https://dev.to/gabriela_colombo_437a7a2d/how-i-migrated-a-live-database-to-a-many-to-many-relationship-3pkl</link>
      <guid>https://dev.to/gabriela_colombo_437a7a2d/how-i-migrated-a-live-database-to-a-many-to-many-relationship-3pkl</guid>
      <description>&lt;p&gt;Most articles about database migrations assume you know the final model before you start. In real products, you often don't. You ship a design, it goes to production, and then a customer conversation tells you the design was wrong.&lt;/p&gt;

&lt;p&gt;In 2026, I designed and built a speaker roles feature for an event platform, end to end: database schema, GraphQL API, and changes to both the organizer dashboard and the attendee-facing event app. I shipped it as a one-to-one relationship. After it was live, the requirement changed to many-to-many. This is how I evolved the data model twice without breaking a single existing event, and what I'd ask earlier next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a custom field doing a feature's job
&lt;/h2&gt;

&lt;p&gt;Organizers wanted to label what each speaker does in a session: moderator, host, interviewer. The platform had no concept for this, so organizers worked around it with a generic custom field on the speaker profile. It was free text, it was manual, and nothing in the product understood it.&lt;/p&gt;

&lt;p&gt;The goal was to replace that workaround with a real feature: organizers define roles per event and assign them to speakers.&lt;/p&gt;

&lt;p&gt;The constraint was the hard part. Existing events were already live, with speakers, integrations, and API clients built on the old data model. None of them could break while the new feature rolled out incrementally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: one role per speaker, zero breaking changes
&lt;/h2&gt;

&lt;p&gt;The first design was simple. I added a speaker_role table, scoped to an event, and linked each speaker to one role through a foreign key.&lt;/p&gt;

&lt;p&gt;A foreign key creates an obvious problem: every existing speaker now needs a valid role, and so does every speaker created by a client that has never heard of roles. Three decisions solved it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every event gets a default role, automatically&lt;/strong&gt;. When an event is created, the backend creates its default role in the same flow. No event can exist without one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing speakers were migrated to their event's default role&lt;/strong&gt;. A migration script assigned them before anything depended on the new relationship.&lt;/li&gt;
&lt;li&gt;**roleId **is optional in the API. If a client doesn't send it, the backend assigns the event's default role.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third one is what kept existing integrations working. In the GraphQL schema, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SpeakerRole&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;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;isDefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreateSpeakerInput&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;span class="n"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# Optional on purpose: older clients don't know roles exist&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;roleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&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;And in the resolver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;speakerRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDefaultRoleId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, but it carries the whole compatibility strategy. Clients that know about roles send one. Clients that don't keep working, and their speakers still end up in a valid state. Nobody had to coordinate a release with us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data question: migrate the old field or not?
&lt;/h2&gt;

&lt;p&gt;During implementation, a product question came up. Organizers already had speaker types in that custom field. Should the event app keep showing it, or should the new roles replace it? Showing both would display the same information twice.&lt;/p&gt;

&lt;p&gt;The tempting answer was to migrate the custom field into the new roles. Before proposing that, I checked production data. Only a small minority of speakers had the custom field filled in. And because it was free text, the values varied too much to map reliably to roles. An automatic migration would have produced wrong roles for real events.&lt;/p&gt;

&lt;p&gt;So instead of migrating, I proposed a toggle. Each organizer chooses whether the event app displays the old custom field or the new speaker roles. Organizers who depended on the old field lost nothing. Everyone else got the new feature.&lt;/p&gt;

&lt;p&gt;The lesson: a migration is only as good as the data you migrate. When the source data is unreliable, giving users a choice can be safer than guessing for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: the requirement changed after launch
&lt;/h2&gt;

&lt;p&gt;The backend was fully in production when a meeting with the integrations team surfaced something new: a customer needed speakers to hold multiple roles in the same session. A speaker could be both host and interviewer in the same session.&lt;/p&gt;

&lt;p&gt;The one-to-one design couldn't express that. A speaker pointed to exactly one role.&lt;/p&gt;

&lt;p&gt;The quick fix would have been to duplicate the speaker, one row per role. That didn't work either. The speaker table had a unique index, and other features relied on a speaker existing once. Breaking that assumption would have spread the change far beyond roles.&lt;/p&gt;

&lt;p&gt;The honest conclusion was that the model needed a redesign: a pivot table between speakers and roles, a many-to-many relationship. The real question was how to get there while Version 1 was live and in use.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzu8pskizxmcj0gsdbuy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzu8pskizxmcj0gsdbuy.png" alt=" " width="799" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating to many-to-many without changing the API
&lt;/h2&gt;

&lt;p&gt;I didn't replace the old relationship. I added the new one next to it and moved traffic over in steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the pivot table&lt;/strong&gt;. speaker_role_assignment links a speaker to a role. The old foreign key stays in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start writing to the new model&lt;/strong&gt;. The organizer dashboard populates the pivot table on every role change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill existing data&lt;/strong&gt;. A migration script copies every existing speaker-role link into the pivot table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read new first, fall back to old&lt;/strong&gt;. A dataloader resolves roles from the pivot table and falls back to the old relationship when no assignment exists yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch the event app&lt;/strong&gt;. It reads the new relationship, and the API response shape stays the same.&lt;/li&gt;
&lt;/ol&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzwzvnzn3cyduow2n6oyt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzwzvnzn3cyduow2n6oyt.png" alt=" " width="799" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The old foreign key stays in Version 2, so reads can fall back to it for any speaker not yet in the pivot table.&lt;br&gt;
Because of that, the backfill didn't need to finish before deploying, and no event ever had a window where its speakers lost their roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ask before designing a relationship now
&lt;/h2&gt;

&lt;p&gt;Looking back, Version 1 wasn't a mistake. It solved the problem as it was described at the time. But a few questions, asked earlier, would have saved a redesign:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Can this ever be more than one?" Ask it about every one-to-one relationship, and ask the people closest to customers, not only product. The integrations team knew before anyone else.&lt;/li&gt;
&lt;li&gt;What happens to clients that don't know this feature exists? Make new inputs optional and give them a safe default.&lt;/li&gt;
&lt;li&gt;Can every existing record satisfy the new constraint? If not, create the defaults and migrate before you enforce it.&lt;/li&gt;
&lt;li&gt;Is the data I'm about to migrate trustworthy? Check production before promising a migration.&lt;/li&gt;
&lt;li&gt;Can old and new models coexist? If reads fall back gracefully, the backfill stops being a deadline.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The hardest part of this feature wasn't the schema or the GraphQL types. It was that production never pauses while you change your mind. Existing events, old integrations, and real organizers were using Version 1 the whole time I built Version 2.&lt;/p&gt;

&lt;p&gt;What made it work was treating every change as a transition instead of a replacement: defaults for clients that don't know about roles, a toggle instead of an unreliable migration, and a dataloader that could read both models at once.&lt;/p&gt;

&lt;p&gt;The question I ask now isn't only "Does the new feature work?" It's "Can the system move safely from where it is to where it needs to be, even if where it needs to be changes halfway through?"&lt;/p&gt;

</description>
      <category>software</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>graphql</category>
    </item>
    <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>backend</category>
      <category>softwareengineering</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
