<?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: Sangeetha Kalia</title>
    <description>The latest articles on DEV Community by Sangeetha Kalia (@sangeetha_kalia_cb89c90aa).</description>
    <link>https://dev.to/sangeetha_kalia_cb89c90aa</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%2F4050620%2F885b762a-6440-477b-b40b-75c387f41af7.png</url>
      <title>DEV Community: Sangeetha Kalia</title>
      <link>https://dev.to/sangeetha_kalia_cb89c90aa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sangeetha_kalia_cb89c90aa"/>
    <language>en</language>
    <item>
      <title>Data Migration, Conversion, and ETL — What I Actually Did, Across Two Eras</title>
      <dc:creator>Sangeetha Kalia</dc:creator>
      <pubDate>Fri, 31 Jul 2026 19:26:58 +0000</pubDate>
      <link>https://dev.to/sangeetha_kalia_cb89c90aa/data-migration-conversion-and-etl-what-i-actually-did-across-two-eras-2emj</link>
      <guid>https://dev.to/sangeetha_kalia_cb89c90aa/data-migration-conversion-and-etl-what-i-actually-did-across-two-eras-2emj</guid>
      <description>&lt;h1&gt;
  
  
  Data Migration, Conversion, and Integration — What I Actually Worked On Across Two Eras
&lt;/h1&gt;

&lt;p&gt;The first time I had to move data between two live production systems, I spent three days convinced the migration was clean — until the reconciliation reports ran wrong and we traced it back to a silent key collision at 2am. No error. No rollback. Just bad data sitting quietly in the target table.&lt;/p&gt;

&lt;p&gt;That experience changed how I think about every data movement problem I've touched since.&lt;/p&gt;

&lt;p&gt;Three words get used interchangeably in most engineering conversations. They shouldn't:&lt;/p&gt;

&lt;p&gt;Migration is a one-time move — different system, different schema, run once, reconcile, archive the scripts.&lt;/p&gt;

&lt;p&gt;Conversion is what happens inside a migration when the shape of the data has to change: types, keys, business rules, validation rules.&lt;/p&gt;

&lt;p&gt;Integration is a continuous pattern — data keeps flowing because two systems stay alive over time.&lt;/p&gt;

&lt;p&gt;This post is about how I've worked across all three — in real professional work, and in a hands-on portfolio project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before we start — a calibration
&lt;/h2&gt;

&lt;p&gt;Not every app needs any of this.&lt;/p&gt;

&lt;p&gt;My MERN school-management app, &lt;a href="https://github.com/Sanganu/arichuvadi-clmgt" rel="noopener noreferrer"&gt;&lt;code&gt;arichuvadi-clmgt&lt;/code&gt;&lt;/a&gt;, is a straightforward direct-updates system: board members, instructors, and students all read and write the same MongoDB, and there is nothing to migrate, no schema to convert, and no external system to integrate against.&lt;/p&gt;

&lt;p&gt;If that describes your app, close this tab — you don’t have a data-movement problem, and adding middleware would only buy you complexity you don’t need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rest of this post is about the projects where the shape is genuinely harder&lt;/strong&gt; — two migration/conversion stories drawn from real professional work, plus an integration project (&lt;a href="https://github.com/Sanganu/sf_crm_app" rel="noopener noreferrer"&gt;&lt;code&gt;sf_crm_app&lt;/code&gt;&lt;/a&gt;) I built on a Salesforce Dev org as a portfolio piece.&lt;/p&gt;

&lt;p&gt;A quick scope note, because this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;professional migration/conversion stories&lt;/strong&gt; here are based on real company work&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;schemas and sample table names are illustrative&lt;/strong&gt; so I can preserve client anonymity&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;original company codebases are not public&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;where I link a public Postgres repo, it should be read as a &lt;strong&gt;smaller public mimic of selected migration patterns&lt;/strong&gt;, not as the full proprietary implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-line rule I want you to leave with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Migrate once, when you’re moving between systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Convert inside a migration, when the shape has to change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrate continuously, when both systems have to stay alive together.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three different problems. Three different kinds of engineering discipline. Getting them mixed up is how teams overcomplicate a one-time migration or under-design an ongoing integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1 — Data conversion in the legacy world: PL/SQL, cursors, and dump tables in a Sybase → Oracle amalgamation
&lt;/h2&gt;

&lt;p&gt;A group I worked with had two systems — one running on Sybase, one on Oracle — and the business wanted them amalgamated into a single Oracle schema.&lt;/p&gt;

&lt;p&gt;There was no middleware layer, no streaming pipeline, no managed orchestration platform. There was CSV, PL/SQL, and a lot of careful handling.&lt;/p&gt;

&lt;p&gt;What we had was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CSV files exported from Sybase&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dump tables in Oracle&lt;/strong&gt; — every column declared as &lt;code&gt;VARCHAR2(4000)&lt;/code&gt;, no constraints, no validations, no &lt;code&gt;NOT NULL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PL/SQL scripts&lt;/strong&gt; to read the dump tables and produce the real, typed, constrained rows in the destination tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the ugly varchar-max dump tables?&lt;/p&gt;

&lt;p&gt;Because when the boundary is strict, one bad row from the source can fail the whole load. When the boundary is loose, the load succeeds and the &lt;em&gt;transformation&lt;/em&gt; becomes the place where problems surface — which is exactly where you want them to surface, under controlled exception handling, with a log you can inspect and rerun from.&lt;/p&gt;

&lt;h3&gt;
  
  
  The transformation loop
&lt;/h3&gt;

&lt;p&gt;For each row we needed to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a &lt;strong&gt;composite key&lt;/strong&gt; of the shape &lt;code&gt;MMM + DDD + YYYYMMDD + SEQ&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Apply business-logic rewrites such as status mapping, currency normalization, and department-code validation&lt;/li&gt;
&lt;li&gt;Handle failures &lt;strong&gt;without stopping the batch&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified shape of that loop looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plsql"&gt;&lt;code&gt;&lt;span class="kr"&gt;CREATE&lt;/span&gt; &lt;span class="kr"&gt;OR&lt;/span&gt; &lt;span class="kr"&gt;REPLACE&lt;/span&gt; &lt;span class="kr"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;load_from_dump&lt;/span&gt; &lt;span class="kr"&gt;IS&lt;/span&gt;
    &lt;span class="k"&gt;CURSOR&lt;/span&gt; &lt;span class="n"&gt;c_dump&lt;/span&gt; &lt;span class="kr"&gt;IS&lt;/span&gt; &lt;span class="kr"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kr"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stg_dump_customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v_new_id&lt;/span&gt;     &lt;span class="kt"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;v_seq&lt;/span&gt;        &lt;span class="kt"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;invalid_dept&lt;/span&gt; &lt;span class="k"&gt;EXCEPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="kr"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;c_dump&lt;/span&gt; &lt;span class="kr"&gt;LOOP&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="kr"&gt;SELECT&lt;/span&gt; &lt;span class="nf"&gt;NVL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="mi"&gt;+1&lt;/span&gt; &lt;span class="kr"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_seq&lt;/span&gt;
              &lt;span class="kr"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;
             &lt;span class="kr"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;load_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;v_new_id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;master_code&lt;/span&gt;
                     &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail_code&lt;/span&gt;
                     &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYYMMDD&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dept_code&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SALES&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPS&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HR&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FIN&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
               &lt;span class="kr"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;invalid_dept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="kr"&gt;INSERT&lt;/span&gt; &lt;span class="kr"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dept_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
            &lt;span class="kr"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dept_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.);&lt;/span&gt;

        &lt;span class="k"&gt;EXCEPTION&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;DUP_VAL_ON_INDEX&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
                &lt;span class="kr"&gt;INSERT&lt;/span&gt; &lt;span class="kr"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;load_exceptions&lt;/span&gt;
                  &lt;span class="kr"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DUP_VAL_ON_INDEX&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Composite key collision: &lt;/span&gt;&lt;span class="o"&gt;'||&lt;/span&gt;&lt;span class="n"&gt;v_new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;invalid_dept&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
                &lt;span class="kr"&gt;INSERT&lt;/span&gt; &lt;span class="kr"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;load_exceptions&lt;/span&gt;
                  &lt;span class="kr"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INVALID_DEPT&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown dept_code: &lt;/span&gt;&lt;span class="o"&gt;'||&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dept_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;OTHERS&lt;/span&gt; &lt;span class="kr"&gt;THEN&lt;/span&gt;
                &lt;span class="kr"&gt;INSERT&lt;/span&gt; &lt;span class="kr"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;load_exceptions&lt;/span&gt;
                  &lt;span class="kr"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;SQLCODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SQLERRM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="kr"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not the exact syntax. The point is the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;loose ingest&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;strict transformation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;explicit exception handling&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rerun based on logged failures, not guesswork&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What the log table was really for
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;load_exceptions&lt;/code&gt; table was not a side effect. It was part of the actual deliverable.&lt;/p&gt;

&lt;p&gt;After each run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Group exceptions by reason code&lt;/li&gt;
&lt;li&gt;Review them with the domain owner&lt;/li&gt;
&lt;li&gt;Decide whether to fix the source, extend the mapping rules, or deliberately exclude the row&lt;/li&gt;
&lt;li&gt;Rerun only the resolved cases&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is what made the conversion survivable. Not elegance — discipline.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lasting lesson
&lt;/h3&gt;

&lt;p&gt;This is what data &lt;em&gt;conversion&lt;/em&gt; looked like when business logic lived inside the database.&lt;/p&gt;

&lt;p&gt;You wrote the transformation close to the data, ran it once, reconciled it, and archived it.&lt;/p&gt;

&lt;p&gt;It was not glamorous, but it was honest and operationally clear.&lt;/p&gt;

&lt;p&gt;For a one-time amalgamation, that was the right shape of solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2 — Data migration in the modern world: consolidating two teams onto one Postgres schema
&lt;/h2&gt;

&lt;p&gt;Fast-forward to a later project. Different tooling, same underlying problem: two teams, two datasets, one target schema.&lt;/p&gt;

&lt;p&gt;This chapter is based on a &lt;strong&gt;real company migration in a Postgres environment&lt;/strong&gt;, but the examples here are anonymized and simplified for public writing.&lt;/p&gt;

&lt;p&gt;A separate public repo, &lt;a href="https://github.com/Sanganu/dataMigration" rel="noopener noreferrer"&gt;&lt;code&gt;dataMigration&lt;/code&gt;&lt;/a&gt;, shows a &lt;strong&gt;much smaller Supabase/Postgres mimic&lt;/strong&gt; of some of the same migration ideas. It is a reference implementation, not the original company codebase or the full migration scope.&lt;/p&gt;

&lt;p&gt;Two teams. Different assumptions from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team A&lt;/strong&gt; used &lt;strong&gt;email address as the primary key&lt;/strong&gt; across much of its data model. Types were inconsistent and naming conventions varied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team B&lt;/strong&gt; used &lt;strong&gt;UUID as the primary key&lt;/strong&gt;, cleaner naming, and more standardized typing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The business wanted a single Postgres schema.&lt;/p&gt;

&lt;p&gt;Before any migration script could be trusted, we first had to answer the harder question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What should the target schema actually be?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Phase 1 — schema design analysis
&lt;/h3&gt;

&lt;p&gt;The first phase was not coding. It was analysis.&lt;/p&gt;

&lt;p&gt;We walked the tables, checked overlaps, identified duplicates, and agreed on a unified target model.&lt;/p&gt;

&lt;p&gt;The rules we settled on looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UUID everywhere&lt;/strong&gt; as the target primary-key strategy&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;standardized naming&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;data-type standardization&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;canonical entities instead of duplicate business tables&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Halfway through that schema review, two additional requirements came in from leadership:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;soft delete on every entity&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;audit logging for field-level traceability&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those sound small when written as bullets. They are not small.&lt;/p&gt;

&lt;p&gt;They affect table design, repository logic, update paths, delete semantics, and validation.&lt;/p&gt;

&lt;p&gt;I was personally involved in the requirement discussions, schema review, migration planning, and technical direction around those changes, while implementation was carried out across the DB team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2 — dependency-ordered migration
&lt;/h3&gt;

&lt;p&gt;Once the target schema was agreed, the data move followed a strict dependency order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phase 1:  users
phase 2:  categories
phase 3:  addresses
phase 4:  products
phase 5:  orders
phase 6:  order_items
phase 7:  reviews
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important idea was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parents first&lt;/li&gt;
&lt;li&gt;children second&lt;/li&gt;
&lt;li&gt;grandchildren after that&lt;/li&gt;
&lt;li&gt;foreign keys resolved through deterministic mappings&lt;/li&gt;
&lt;li&gt;reruns must be safe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between a migration and a pile of hopeful SQL files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3 — patterns that made the migration survivable
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A. Build the ID mapping once
&lt;/h4&gt;

&lt;p&gt;New UUIDs should be minted in exactly one place and reused downstream through mapping logic. Otherwise you invite duplicate IDs and orphaned children.&lt;/p&gt;

&lt;p&gt;A simplified shape looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tmp_user_map&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;old_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'Unknown'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;UPPER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'ADMIN'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'CUSTOMER'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'S'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'STAFF'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'CUSTOMER'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;user_role&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'^&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="s1"&gt;?[0-9&lt;/span&gt;&lt;span class="se"&gt;\s\-&lt;/span&gt;&lt;span class="s1"&gt;()]{7,20}$'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deleted_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team_a_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;~*&lt;/span&gt; &lt;span class="s1"&gt;'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;[A-Za-z]{2,}$'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several important patterns compressed into that one step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LOWER(TRIM(...))&lt;/code&gt; for key normalization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NULLIF(TRIM(...), '')&lt;/code&gt; to handle empty-string cleanup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COALESCE(...)&lt;/code&gt; for defaults&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CASE&lt;/code&gt; for enum normalization&lt;/li&gt;
&lt;li&gt;regex validation for data shape&lt;/li&gt;
&lt;li&gt;carrying deletion state forward instead of losing it&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  B. Make reruns safe
&lt;/h4&gt;

&lt;p&gt;The load had to be idempotent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deleted_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deleted_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tmp_user_map&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;role&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the biggest differences between “a script that runs” and “a migration you can trust.”&lt;/p&gt;

&lt;p&gt;If a batch is interrupted, you need a path to rerun without duplicating or corrupting data.&lt;/p&gt;

&lt;h4&gt;
  
  
  C. Respect dependency order in child loads
&lt;/h4&gt;

&lt;p&gt;Once parent entities existed, child tables could be loaded by joining against previously built maps.&lt;/p&gt;

&lt;p&gt;The exact table names and domain details here are anonymized, but the pattern stayed the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resolve parent IDs first&lt;/li&gt;
&lt;li&gt;join through mapping tables&lt;/li&gt;
&lt;li&gt;allow controlled fallback where needed&lt;/li&gt;
&lt;li&gt;avoid silently losing child rows&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  D. Reconciliation matters as much as the load
&lt;/h4&gt;

&lt;p&gt;Each phase needs reconciliation artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected source counts&lt;/li&gt;
&lt;li&gt;loaded target counts&lt;/li&gt;
&lt;li&gt;validation failures&lt;/li&gt;
&lt;li&gt;unresolved references&lt;/li&gt;
&lt;li&gt;rows held for second-pass cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the migration is not finished when the insert completes.&lt;/p&gt;

&lt;p&gt;It is finished when the business can trust the reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  One detail that matters more than it looks: empty strings vs nulls
&lt;/h3&gt;

&lt;p&gt;One of the recurring cleanup issues in migrations is how systems represent “missing” values.&lt;/p&gt;

&lt;p&gt;For example, legacy text columns often contain &lt;code&gt;''&lt;/code&gt; where the target logic expects &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;casts can fail&lt;/li&gt;
&lt;li&gt;uniqueness can behave differently&lt;/li&gt;
&lt;li&gt;joins can behave differently&lt;/li&gt;
&lt;li&gt;validation can behave differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safe pattern looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dob&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dob&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of cleanup is small in code but large in effect.&lt;/p&gt;

&lt;p&gt;One careful &lt;code&gt;CASE&lt;/code&gt; can save an entire batch from failing on dirty source data.&lt;/p&gt;

&lt;h3&gt;
  
  
  What made this migration workable
&lt;/h3&gt;

&lt;p&gt;Four principles carried the project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Loose ingest, strict transformation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A single mapping source of truth&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency-ordered loading&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotent reruns&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything above describes a &lt;strong&gt;one-time migration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once the consolidation was complete, the scripts were archived and the unified schema became the system of record.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3 — Seven practical migration techniques I keep coming back to
&lt;/h2&gt;

&lt;p&gt;If I strip away all project-specific detail, the migration side keeps collapsing into a few repeatable patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Preserve valid UUIDs, generate new ones for invalid IDs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;~*&lt;/span&gt; &lt;span class="s1"&gt;'^[0-9a-fA-F-]{36}$'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; don’t rewrite a valid key unnecessarily.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Normalize empty strings before casting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dob&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dob&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; dirty text columns should not kill the batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use idempotent upsert patterns
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&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;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; partial failure becomes recoverable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Preserve legacy IDs when they help reconciliation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category_uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; migration is easier to validate when you keep a bridge back to the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Use placeholder foreign keys when a second pass is required
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category_uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; not every relationship can be resolved in one pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Resolve foreign keys through deterministic joins
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addresses&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;street&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&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="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;du&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;street&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addresses&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;du&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;du&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; children should attach to already-migrated parents in a traceable way.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Store money in integer cents, not floating-point dollars
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;original_price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;original_price_cents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; exact arithmetic beats cleanup later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4 — Integration when both systems stay alive
&lt;/h2&gt;

&lt;p&gt;Migration and conversion are one-time efforts.&lt;/p&gt;

&lt;p&gt;Integration is different.&lt;/p&gt;

&lt;p&gt;Data keeps moving because the systems on both sides remain active.&lt;/p&gt;

&lt;p&gt;My portfolio project &lt;a href="https://github.com/Sanganu/sf_crm_app" rel="noopener noreferrer"&gt;&lt;code&gt;sf_crm_app&lt;/code&gt;&lt;/a&gt; demonstrates that pattern as a &lt;strong&gt;MuleSoft ↔ Salesforce operational integration&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope, explicitly:&lt;/strong&gt; In this repo I built the MuleSoft-to-Salesforce slice — the MuleSoft flow (HTTP listener, OAuth2, DataWeave transform, PATCH upsert) and the Salesforce side (Apex triggers, LWC, custom objects, and tests). The upstream side is simulated by an HTTP payload posted to MuleSoft, not a live external system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Salesforce data model
&lt;/h3&gt;

&lt;p&gt;Three custom objects, external-ID enabled for rerunnable upserts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Student__c        GPA, Status, Student_External_Id__c, enrollment history
Course__c         Capacity, Credits, Department, Active flag
Enrollment__c     junction ↔ Student__c ↔ Course__c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The MuleSoft flow
&lt;/h3&gt;

&lt;p&gt;The flow handles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;receiving a simulated upstream payload&lt;/li&gt;
&lt;li&gt;authenticating into Salesforce&lt;/li&gt;
&lt;li&gt;transforming field names and shapes&lt;/li&gt;
&lt;li&gt;upserting by External ID&lt;/li&gt;
&lt;li&gt;logging per record so one bad record does not kill the whole batch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified shape of the transform looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%dw 2.0
output application/json
---
{
  Student_External_Id__c: payload.STU_ID,
  FirstName__c:           payload.FIRST_NAME,
  LastName__c:            payload.LAST_NAME,
  Status__c:              payload.STATUS default "Active",
  GPA__c:                 payload.GPA as Number { format: "##.##" }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core idea is simple: the external system and Salesforce do not speak the same field language, so the integration layer translates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Business rules still belong to the owning system
&lt;/h3&gt;

&lt;p&gt;Even after the record lands in Salesforce, some rules still belong there because Salesforce owns the current operational state.&lt;/p&gt;

&lt;p&gt;A simplified Apex example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apex"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="kd"&gt;sharing&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnrollmentTriggerHandler&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;TriggerHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;beforeInsert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;courseIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enrollment__c&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Enrollment__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;courseIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Course__c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Course__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;byId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Course__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Capacity__c&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="n"&gt;Id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Enrollments__r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Course__c&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="n"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;courseIds&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;USER_MODE&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enrollment__c&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Enrollment__c&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Trigger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;Course__c&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Course__c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Enrollments__r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Capacity__c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Status__c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s2"&gt;Waitlist'&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same broad lesson as the earlier conversion story: data movement alone is not enough.&lt;/p&gt;

&lt;p&gt;You also need to know &lt;strong&gt;where the business rule belongs&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this integration project does and does not cover
&lt;/h3&gt;

&lt;p&gt;What it &lt;strong&gt;does&lt;/strong&gt; include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MuleSoft flow design&lt;/li&gt;
&lt;li&gt;DataWeave transformation&lt;/li&gt;
&lt;li&gt;External-ID upsert into Salesforce&lt;/li&gt;
&lt;li&gt;Apex business logic&lt;/li&gt;
&lt;li&gt;LWC components&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it &lt;strong&gt;does not&lt;/strong&gt; include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a live upstream production system&lt;/li&gt;
&lt;li&gt;a full enterprise integration landscape&lt;/li&gt;
&lt;li&gt;every system that might sit around the CRM in a real institution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That boundary matters.&lt;/p&gt;

&lt;p&gt;Public portfolio code should show the slice you built, not pretend to be the whole organization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 5 — What stays the same across all three
&lt;/h2&gt;

&lt;p&gt;The tools change.&lt;/p&gt;

&lt;p&gt;The engineering concerns don’t.&lt;/p&gt;

&lt;p&gt;Whether the work is a legacy conversion, a one-time migration, or an ongoing integration, I keep coming back to the same four constants.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prevent data loss
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;know where partial failures land&lt;/li&gt;
&lt;li&gt;know what commits and what rolls back&lt;/li&gt;
&lt;li&gt;know how to rerun safely&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Protect data integrity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;respect foreign keys&lt;/li&gt;
&lt;li&gt;validate target shape&lt;/li&gt;
&lt;li&gt;do not trust dirty source data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Make failures visible
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;exception logs&lt;/li&gt;
&lt;li&gt;reconciliation reports&lt;/li&gt;
&lt;li&gt;explicit review paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Design for idempotency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;safe reruns&lt;/li&gt;
&lt;li&gt;conflict-aware writes&lt;/li&gt;
&lt;li&gt;deterministic mappings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The names of the tools change.&lt;/p&gt;

&lt;p&gt;The job does not.&lt;/p&gt;

&lt;p&gt;A PL/SQL exception log, a Postgres reconciliation pass, and a rerunnable External-ID upsert are all solving the same deeper problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;never let failure become invisible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;never let a retry become corruption&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Five review questions I bring to every design discussion
&lt;/h3&gt;

&lt;p&gt;Regardless of the stack, I want clean answers to these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where does a partial failure land?&lt;/li&gt;
&lt;li&gt;Is the write safe to repeat?&lt;/li&gt;
&lt;li&gt;Are the relationships enforced or merely assumed?&lt;/li&gt;
&lt;li&gt;Where do unexpected exceptions surface?&lt;/li&gt;
&lt;li&gt;What is the actual system of record?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a design is weak on those five, it is weak no matter how modern the tooling looks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion — the tools change, the engineering does not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Migration&lt;/strong&gt; is a one-time move.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Conversion&lt;/strong&gt; reshapes the data inside that move.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Integration&lt;/strong&gt; keeps data flowing when both systems remain alive.&lt;/p&gt;

&lt;p&gt;Confusing those three leads to expensive mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;building a long-lived integration for a one-time move&lt;/li&gt;
&lt;li&gt;treating ongoing operational sync like a one-off script&lt;/li&gt;
&lt;li&gt;ignoring reconciliation because “the insert succeeded”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across all three, the engineering discipline stays the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never lose a record.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never break a relationship.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never hide an exception.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never write a load you can’t safely rerun.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Get those right, and the architecture gets easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Public reference repos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Integration portfolio project: &lt;a href="https://github.com/Sanganu/sf_crm_app" rel="noopener noreferrer"&gt;https://github.com/Sanganu/sf_crm_app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Direct-updates counter-example (MERN, no migration/integration layer needed): &lt;a href="https://github.com/Sanganu/arichuvadi-clmgt" rel="noopener noreferrer"&gt;https://github.com/Sanganu/arichuvadi-clmgt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Postgres migration reference mimic (a smaller public illustration of selected migration patterns): &lt;a href="https://github.com/Sanganu/dataMigration" rel="noopener noreferrer"&gt;https://github.com/Sanganu/dataMigration&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you've dealt with a messy migration or integration — I'd genuinely like to hear what patterns you reached for. Drop it in the comments.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Written by Sangeetha Kaliaperumal · working across PL/SQL, Postgres, MERN, and the Salesforce ecosystem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
