1. Scope & scale
The new platform holds 82 more tables than the legacy one. They fall into three groups:
- Capabilities the legacy system never had — external system integration, content AI analysis, read models, content management.
- Existing capabilities modelled in more detail — inspection grew from 11 tables to 34; reference data from 74 to 83.
- Extension tables — highly variable data such as “what the applicant typed into the form” moved out of fixed columns into a structured document held in an extension table, one per main table.
What happens to the 215 legacy tables
| Disposition | Tables | Meaning |
|---|---|---|
| Reprocessed, then loaded rules applied | 17 | The core business: applications, licences, certificates, user profiles. |
| Projected, then loaded columns rewritten | 20 | Structurally close; columns and value ranges are adjusted on the way through. |
| Copied as-is | the remainder | Identical structure on both sides — no transformation needed. |
| Explicitly not migrated by decision | 116 | See below. Every one of them carries a written reason. |
Why 116 tables are not migrated
| Reason | Detail |
|---|---|
| Replaced by new reference data (33) | The new platform builds its dictionaries from a curated standard seed. Legacy dictionary values accumulated over the years are not carried over. |
| Legacy migration helpers (19) | Tables the old system used for its own past migrations. Not business data. |
| Superseded by a redesign | Inspection (11), payment (7), workflow (3) and others. The data does move — but rebuilt into the new structure, not copied table-for-table. |
| Out of business scope | External CRM, environment configuration, temporary tables. |
Reference data is seeded, not inherited
Dictionaries, the service catalogue, permissions, inspection standards and report definitions are all built from curated standard data in the new platform. If a legacy dictionary value must be preserved for business reasons, it needs to be raised in advance and added to the seed — it will not arrive by itself.
2. The three databases
| Database | Role | Write access |
|---|---|---|
| Legacy | The only source of data. Nothing is invented. | Read-only. The single exception is a handful of temporary performance indexes and migration-only staging tables, which retire with the legacy system. |
| Staging | The workshop. Holds every intermediate result while renumbering, splitting and de-duplication happen. | Fully writable. Completely rebuilt at the start of every run — nothing survives between runs. |
| New | The destination the platform actually serves. | Fully writable. Emptied before loading, so a rerun never mixes with a previous attempt. |
In production these three databases may sit on different servers with no direct link between them. That single constraint shapes the whole design: no query can join across them, so every cross-database movement goes through an export-file handover, and any rule that needs data from the other side must first bring a copy of that reference data across. It costs extra steps in the log, but it is invisible from a business point of view.
3. The seven steps
The whole migration runs as a single command with no manual intervention in the middle. In business terms it is seven stages.
-
Health check gate
Before anything moves: can all three databases be reached, is there enough disk space, does the configuration match the files actually present, and does every extraction statement line up column-for-column with its destination table?
The run is not allowed to start unless the health check is fully green. This is deliberate — discovering a configuration error three hours in costs those three hours.
-
Clear the floor
The staging database is rebuilt from scratch and the new database is emptied, so every run starts from a genuinely clean state.
The new platform’s database triggers are switched off at this point. Triggers exist to support day-to-day business automation; during a bulk load of millions of rows they are both very slow and capable of firing incorrect side effects. They are switched back on once loading completes.
-
Lay the foundation
Reference data first: service catalogue, permission model, dictionaries, notification templates, fee configuration, inspection standards, report definitions.
Order matters here. The service catalogue must be in place before anything else, because every application that follows depends on it to answer “what kind of business is this?”.
-
Move the people and the entities
User accounts, user profiles, establishments, natural persons, addresses, establishment partners. Two business rules are worth calling out:
- Profile de-duplication — the legacy system allowed one person to hold several profile records. These are merged under a fixed rule, and the profile’s type and status are recalculated from whether the user is an individual or an establishment and whether they have ever been approved.
- Establishment profile cloning — the new platform requires one profile to belong to exactly one establishment. A legacy user managing several establishments is split into several establishment profiles.
-
Move the applications most complex
The core of the migration and the longest-running stage. It breaks down into five actions, covered in sections 4 and 5 below: decide the service, rebuild the application chain, rebuild the workflow, restore the form content, and archive whatever genuinely cannot be migrated.
-
Move the remaining business areas
Content library, enquiries, supervisor reports, inspection and payment. Each has its own remapping logic because each was redesigned in the new platform.
These five share the cross-server constraint from section 2, so each runs as a three-leg operation: export the reference data it needs to the legacy side, do the work locally there, then bring the result back. No business impact — it just explains why these stages show more steps in the log.
-
Finishing and back-fill
- Phone numbers split into country code and local number;
- Establishment type derived from the user profile type;
- Establishment partners de-duplicated;
- Establishment emirate repointed to the new reference data;
- Legacy attachments registered in the unified file table;
- Remaining gap-filling, cleaning and dirty-data correction;
- Triggers switched back on — the platform returns to normal operating state.
4. Deciding what each application is
The legacy system identified a service by a numeric service ID. The new platform uses a ServiceCode as its business identity anchor. The two are not a simple lookup: the same legacy service ID can map to different new service codes depending on who applied and what they applied for.
So there is a dedicated routing stage that decides, line by line, which service each application detail belongs to. It works in three tiers:
| Tier | Rule | How it decides |
|---|---|---|
| 1 | Manual override highest priority | A business-maintained mapping table for the exceptions the automatic rules cannot judge. Business confirms the case, the row is entered, and no code changes. |
| 2 | Content of the application detail | The characteristics of what was actually applied for — e.g. publication type combined with printing type splits a legacy printing permit into script, movie, book, newspaper or magazine services. |
| 3 | Applicant type and economic activity | General rules based on who is applying (individual, establishment, government) and the registered economic activity behind the licence. |
Once the base service code is known, the lifecycle is applied: whether this is a new, renewal or amendment request determines the final service code, because in the new platform the lifecycle is part of the service identity rather than a separate field.
What if it cannot be decided?
Nothing is discarded silently. An application detail whose service cannot be determined is written to an unresolved ledger with the reason, and — for the categories that would corrupt downstream data — the migration stops rather than continues. Unresolved routing is treated as a defect to fix, not as acceptable loss.
Why this matters more than it looks
Every business decision in the new platform reads the ServiceCode: which workflow runs, which certificate template is issued, which fee applies, which team sees the task, and how it is counted in reports. A routing error does not produce a visibly broken record — it produces a record that quietly sits under the wrong service. This is the single area where business review of the results pays off the most.
5. Rebuilding the application chain
5.1 New numbering, with one deliberate exception
Applications, application details, licences, certificates and the licence index all changed both their numbering scheme and the way they link to each other. The migration builds an old-to-new number mapping table, and every relationship is reconnected through it.
One exception is kept on purpose: the application detail number is preserved exactly as it was in the legacy system. A staff member holding a legacy reference number can look it up directly in the new platform.
One legacy detail becomes one new application
In the legacy system a single application could contain several details. The new platform flattens this: each detail becomes an application in its own right. Total counts will therefore look different between the two systems — this is by design, and the audit checks the relationship rather than a raw equality.
5.2 Rebuilding the workflow
The legacy system ran approvals on its own state machine. The new platform uses the Camunda process engine. Historical cases obviously cannot be re-executed through Camunda, so the migration takes a different approach: it synthesises the matching process instances and task nodes from the historical approval record, so what the new platform displays agrees with what actually happened.
The practical consequence: a historical case shows its full approval trail in the new platform, but the synthesised process cannot be pushed further through Camunda. For a completed case that is not a limitation. For an in-flight one it is — see section 9.
5.3 Restoring the form content
The legacy system scattered what the applicant filled in across many different columns and tables. The new platform stores it as one structured form document. The migration reassembles those scattered fields into a complete form per service template, so that opening a historical application in the new platform shows what was originally submitted.
5.4 Nothing disappears silently
Some details genuinely cannot be migrated — the service cannot be determined, or a mandatory link is missing in the source. These are not dropped quietly. Each one is written to a discard ledger with its reason, and the audit then reconciles the ledger against the source:
Every legacy application detail is either in the new database, or in the discard ledger with a stated reason. Vanishing is not an accepted outcome.
The discard ledger is part of the delivery. It needs a business review — the point is for the business to confirm “yes, these genuinely do not need to move”, rather than for that to be decided technically.
6. Before & after, by business area
This is the part that matters most day to day: the same thing, in a different shape.
6.1 Services and business identity
| Legacy | New platform | |
|---|---|---|
| Business identifier | Numeric service ID | ServiceCode |
| Service catalogue | Legacy catalogue tables | Rebuilt standard catalogue |
| Lifecycle | A separate field | Folded into the service code — new, renewal and amendment each have their own code |
Impact: report definitions, permission configuration and fee rules all need to be realigned onto service codes. The legacy ID is retained for traceability but never drives a business decision.
6.2 Users and establishments
| Legacy | New platform | |
|---|---|---|
| User profile | Could exist several times over | Merged and de-duplicated; type and status recalculated |
| One person, several establishments | One profile linked to many | Split into several profiles — one profile, one establishment |
| Establishment type | Its own dictionary | Derived from the user profile type |
| Permissions | Legacy groups and roles | Rebuilt department + role + permission model |
Impact: a user may now see several establishment identities after login and has to choose which one they are acting for. That is the design, not duplicated data.
6.3 Applications
| Legacy | New platform | |
|---|---|---|
| Application number | Legacy number | Renumbered (old-to-new mapping retained) |
| Application detail number | Legacy number | Unchanged — legacy reference still works |
| Form content | Scattered across columns | One structured form document |
| Extended information | — | New extension tables (self-monitoring snapshot, AI analysis results) |
6.4 Workflow and approvals
| Legacy | New platform | |
|---|---|---|
| Process engine | In-house state machine | Camunda |
| Historical processes | State machine records | Process instances and tasks synthesised from the approval history |
| Approval records | Legacy approval table | Rebuilt records attached to the synthesised tasks |
6.5 Certificates and licences
| Legacy | New platform | |
|---|---|---|
| Certificates | One certificate table | Certificate + certificate detail |
| Licence binding | Loose association | Each application is bound to its own licence version |
| Licence index | — | New licence index — a single lookup surface |
| Certificate files | URL columns spread across tables | One unified file registry |
Impact: “which valid licences does this party currently hold?” is answered from the licence index from now on.
6.6 Inspection and payment
| Legacy | New platform | |
|---|---|---|
| Inspection | 11 tables | 34 tables — tasks, violations, checklists and penalties modelled separately |
| Inspection standards | Configured in several places | One seeded penalty standard and checklist template set |
| Payment | 7 tables | 15 tables — transactions, receipts and third-party payment records separated |
6.7 Content business
| Legacy | New platform | |
|---|---|---|
| Book approval status | Yes / no | A re-coded status enumeration |
| Content ledger | Spread across content tables | Unified content library view + content team read model |
| AI analysis | — | New content AI analysis results |
7. How data physically moves
Four mechanisms, chosen per table according to how much reshaping it needs.
Reprocessed load 17 tables
A transformation procedure builds the result in the staging database; it is then exported and loaded into the new database. Row counts are compared on both sides immediately after every table.
Projected direct load 20 tables
A tailored extraction statement runs against the legacy database and its result is loaded straight into the new one. Each attempt resets the destination table first, commits in batches, and retries up to three times with a backoff.
Three-leg cross-server load
Used for content library, enquiries, supervisor reports, inspection and payment: export the reference data the rules need from the new database to the legacy side, run the rules locally, then bring the result back. This turns an impossible cross-server join into a local indexed one — for supervisor reports it cut a 1.5–2 hour stage down dramatically.
Automatic straight copy
Every table that is neither in a managed list nor in the do-not-migrate list is copied as-is in parallel. Identical structure on both sides, so no projection is needed.
The failure mode we design against: no error, but fewer rows
In a bulk load the dangerous outcome is not a crash — it is a silent shortfall. There are three independent guards against it:
- Row-count comparison on every transfer. Exported rows must equal loaded rows, per table, or the run stops.
- Statement-count comparison on every reference-data script. If a script contains 442 insert statements, the table must grow by at least 442 rows.
- Full result draining. A real incident sits behind this one: a 442-row reference-data load once inserted only 318 rows with zero errors reported. That case is now caught immediately.
8. How we prove it is correct
“It finished” is not the same as “it is right”. There are three layers of verification.
Layer 1 — In-flight guards
The row-count and statement-count checks described above. They run during the migration and stop it on the spot rather than letting a shortfall flow downstream.
Layer 2 — Deep audit at the end of the run
The final stage of every migration runs an automated audit: 11 groups, 60+ individual checks, ending with an N/M checks passed summary and the offending records listed for anything that fails.
| Group | What it checks |
|---|---|
| A | Accounts and profiles (12 checks) — de-duplication rules, status derivation, establishment link uniqueness |
| B | Service routing (4 checks) — was the service code decided correctly |
| C | Application chain (19 checks) — the largest group; see below |
| D | Migration configuration completeness |
| F | Enquiry cascade |
| G | Arabic and Unicode encoding guard |
| H | Content library derived data |
| I | Establishment partner de-duplication invariants |
| J | Establishment emirate repointing |
| K | Camunda workflow configuration completeness |
| L | Renewal activity visibility |
(There is no group E — the lettering is historical.)
The three most important checks in group C:
- Nothing vanishes — every legacy application detail is either in the new database or in the discard ledger.
- Detail numbers are unchanged — a legacy reference number still resolves.
- Totals reconcile — the application count in the new database equals the row count of the old-to-new mapping table.
Group G deserves its own mention: the platform holds a large amount of Arabic content, and bulk import is precisely where character corruption appears. The audit samples Arabic fields specifically to confirm they arrived intact.
Layer 3 — Independent verification
The audit is the migration checking its own work. To avoid the blind spot of “the same logic both performing and validating the work”, there is a separate verification tool with an independent implementation that re-checks the results by a different route. Acceptance is signed off against this layer, not against the built-in audit.
9. Where it breaks most often
In-flight applications are the fragile part
An application that was still moving through the legacy workflow at the moment of cut-over — typically Pending Modification or Pending External Approval — carries live workflow state that has no direct equivalent in the new platform. It can land without its current approval node, without its pending task, or without the form snapshot the customer was asked to correct.
Completed and rejected applications migrate cleanly. The in-flight ones need to be re-checked case by case after every migration run. This is the area to concentrate business testing on.
Two secondary areas worth attention during validation:
- Service routing edge cases — an application under the wrong service code looks perfectly healthy on screen. Spot-checking a sample per service is worth more here than checking totals.
- Historical dirty data now becoming visible — the legacy system hid some duplicates by filtering them only when reading. The new platform does not. Where that happens, the position taken is that dirty data is cleaned during migration and the application code stays simple — for example duplicate establishment-partner links are physically removed during the run rather than filtered at display time.
10. When something goes wrong
The run is interrupted
The migration supports resuming from the point of interruption — there is no need to repeat the hours of work already completed. Each resume path clears and reloads only its own scope, so resuming never doubles up data.
One business-relevant exception: resuming from the reference-data stage does not re-run the form restoration. If the interruption happened after form restoration, confirm whether the form data needs to be completed separately.
The data looks wrong
Suggested order of investigation:
- Read the audit report first — a failed check names the exact record and the exact rule.
- Then check the discard and unresolved ledgers — if a record “is not there”, it is most likely in one of these two, with a reason attached.
- Only then question the migration logic — the first two steps locate the large majority of issues.
Just looking, without touching anything
There are read-only diagnostic commands: check space, print the execution plan, run the audit alone. None of them change any data, so they are safe to run against a live environment.
Timing and attribution
Every run reports how long each stage took and, if it failed, which stage it failed in and after how long. Failures are also classified — an environment-level problem such as a full disk volume is labelled as such, so it goes to the DBA rather than being investigated as a mapping defect.
11. What we need from the business
-
Confirm the service code mapping
The highest-priority routing rule is the manually specified mapping. That table is owned by the business: when an automatic rule cannot judge an exception, the business confirms it and the row is entered. No code change is needed.
-
Review the discard ledger line by line
The migration honestly lists everything it could not process. That list needs business confirmation that these records genuinely do not need to migrate — that call should not be made technically.
-
Raise any legacy reference values that must survive
Dictionaries, the service catalogue, permissions, inspection standards and report definitions are built from standard seed data and do not inherit legacy history. If a specific legacy value must be preserved, it has to be raised in advance so it can be added to the seed.
And one thing to plan for
Because in-flight applications are the known weak point (section 9), the most valuable use of business testing time after each migration run is a case-by-case walk through the applications that were mid-workflow at cut-over — not a broad sweep of completed history.
12. Table by table — rebuilt by rule (17 tables)
These seventeen tables cannot be copied. Each one is produced by a transformation procedure inside the staging database, checked there, and only then loaded into the new platform. The right-hand column is the business rule that actually runs.
12.1 People and accounts
| Table | What it holds | Business logic applied |
|---|---|---|
Account.UserProfiles | The customer profile behind each login | The legacy system allows one user to have several profiles; the new platform allows one. The earliest profile is kept and the rest are dropped. A primary establishment is chosen per user (lowest establishment, then oldest link) and that establishment's address is attached to the profile. Account status is derived, not copied: a user who has ever been issued a certificate is marked approved. This replaced the legacy "state = 1" test, which in the legacy dictionary actually meant Saved and therefore almost never matched. Having held a certificate is direct evidence of approval; whether it is still valid does not matter (confirmed by the business). |
Account.UserEstablishments | Which user belongs to which establishment | Rebuilt against the new profile numbering, carrying the "legal representative" flag. This is the link the establishment type is later derived from. |
Account.AdminUsers | Back-office staff accounts | Must be rebuilt before the applications, because every approval task resolves its owner from this table. Two new mobile columns (country code / local number) are created empty and filled later by the phone-splitting step. If this table were empty, every historical task would come across unassigned — so the run explicitly warns when that happens. |
Account.UserActionsTaken | What each user did | Carried across so user history is not lost with the legacy system. |
Lookup.NotificationSendRecord | Notifications sent to users | The legacy system has no notification ledger. The records are derived from the legacy user-action log, so notification history still exists in the new platform. |
Lookup.Prefixes | ISBN publisher prefixes | Restored from legacy rather than re-seeded — these are numbers that were actually issued to publishers and must not be regenerated. |
12.2 The application chain
| Table | What it holds | Business logic applied |
|---|---|---|
Application.Applications | The application record itself | The spine of the migration. One legacy application line becomes one new application (a legacy application could contain several lines; the new platform keeps them separate). Twenty-one columns are written. The service code is produced by the routing rules in section 4 and becomes the business identity of the record — everything downstream branches on it. The licence link, the seven self-monitoring approval snapshot columns, and the origin fields (source system, external reference) are all populated here. |
Extensions.ApplicationsExt | Extension record, one per application | Created one-to-one with the application; holds fields the legacy system never had, including the AI-analysis queue fields, which start empty. |
Application.ApplicationDetails | The detail line of the application | Renumbered onto the new application. The legacy line number is retained in the staging database only, as the old ↔ new cross-reference used by the audit. |
Extensions.ApplicationDetailsExt | What the applicant actually filled in | The application procedure creates only an empty shell; a dedicated second procedure then assembles the submitted form as a structured document, in nine batches — one per family of services, because no two service forms share the same fields. File references inside the form are normalised with exactly the same rule used by the file register, so a document referenced on the form resolves to the stored file when the user clicks it. Before this procedure was introduced, these two columns came across completely empty. |
Application.MediaLicenses | Media licences | Rebuilt with the application chain so that licence, application and certificate stay consistent with each other. Where a renewal or amendment lost its link to the original licence, the link is restored from the lineage family. |
Application.Certificates | Certificates issued | Rebuilt against the new application numbering. The certificate is also what decides whether the owning user profile counts as approved (12.1). |
Application.LicensePermitIndex | The licence / permit index — the "what does this entity currently hold" view | Rebuilt from scratch, not copied. Applications that belong to the same lineage (an original licence plus its renewals and amendments) are merged into a single index entry; where two members of a family disagree, the conflict is counted and reported rather than silently resolved. Rows that cannot be indexed go to a discard ledger. |
12.3 Workflow and approvals
| Table | What it holds | Business logic applied |
|---|---|---|
Workflow.CamundaProcessInstances | Workflow instances | The legacy state machine is retired entirely. For every migrated application a process instance is synthesised in the new workflow engine, so that historical cases still open and still show a workflow. |
Workflow.CamundaTasks | Workflow tasks | An anchor task is synthesised per application. The task owner is resolved through the back-office user table; where legacy has no identifiable owner, the task is deliberately left unassigned rather than being given to an arbitrary person, and the run prints how many were resolved and how many were not. |
Workflow.ApprovalRecords | Approval history | Rebuilt from the legacy action log. Where the legacy status and the legacy workflow state contradict each other, the last recorded action decides the final status (a cancel action ends the case as cancelled, an approve action as approved). |
Extensions.ContentAiAnalysisResults | AI content-analysis results | The rows are created from the legacy book records so the new AI feature has something to attach to; the analysis results themselves start empty and are produced by the new platform, not migrated. |
Why ten of these load together
Applications, their extensions, details, form content, licences, certificates, the licence index and the three workflow tables all reference each other. They are therefore held back from the general loading stage and loaded as one dependency-ordered batch once the whole chain has been rebuilt and verified in staging. Loading them one at a time would create moments where an application exists without its licence, or a task without its application.
13. Table by table — rewritten in transit (20 tables)
These tables are structurally close on both sides, so no procedure is needed — but they cannot be copied verbatim either, because the new platform added columns, tightened data types or renumbered dictionary values. Each one is read through a custom projection that reshapes it on the way out.
| Table | Rewrite applied |
|---|---|
Account.Users | Login accounts. The lockout flag is forced off for every migrated user, so nobody arrives in the new platform locked out by a legacy value. New columns are added empty. |
Account.UserLogins | External login links. Orphan rows are filtered out — legacy contains login records whose user no longer exists, and loading them would break referential integrity. Login type, login method and language are added. |
Account.UAEPassLinks | UAE Pass links. Same orphan filter: links pointing at a user that does not exist are not migrated. |
Core.Persons | Natural persons. Nationality and gender are converted explicitly to the new dictionary types. |
Core.Establishments | Establishments. The licence number is normalised to text because legacy stored it inconsistently across records. Establishment type is deliberately not taken from the legacy dictionary — it is derived after loading from the owning profile's user type (section 15), because the legacy column is empty for almost every row and leaving it empty blanks out the establishment page. The emirate is also re-pointed after loading. |
Core.EstablishmentPartners | Establishment partners. New representative columns are added. Duplicate partner links, and rows that point at neither a person nor an establishment, are physically deleted after loading — the legacy system hid them by de-duplicating when reading, the new platform reads plainly, so the clean-up is done once during migration instead of being worked around in the application. |
Core.Newspapers | Newspapers. Carries a derived column computed from related records rather than a stored legacy value. |
Core.TVs | TV entities. Capital defaults to zero where legacy left it empty, so financial columns are never null. |
Core.ChiefEditors | Chief editors. Phone country-code / local-number columns are added and filled later by the phone-splitting step. |
Core.ForeignEntity | Foreign entities. Same phone split as above. |
Core.Books | Books. The approval flag is re-mapped, not copied: legacy 1 stays 1, legacy 0 becomes 2, empty stays empty — the new platform uses a different set of values. AI-analysis columns are added empty. |
Application.MediaLicenseEconomicActivities | Licence ↔ economic activity. One value re-mapping: economic activity 1042 becomes 2037, because that activity was renumbered in the new catalogue. |
Application.CertificateDetails | Certificate details. Expiry date is converted safely — an unparseable legacy value becomes empty instead of failing the whole table. Cancellation date is added empty. |
Application.PhotographyPermits | Photography permits. New business columns (requesting party, executing party, address) are added for the new form. |
Application.PrintingPermits | Printing permits. Long column-by-column projection onto the new shape. |
Application.RegulateEntriesItems | Regulated entry items. Material reference column added empty; it is populated by the new platform going forward. |
Lookup.Offices | Offices — projected straight across. |
Lookup.Emirates | Emirates — display sort order added. |
Lookup.PrintedType | Printed types — "visible" flag defaults to shown, so nothing disappears from a dropdown after migration. |
Lookup.ProblemCauses | Problem causes — same visibility default. |
The four patterns behind every rewrite
1 · Add an empty column
The new platform has a column legacy never had. It is created empty and filled by a later step (phone splitting is the typical case) or by the new platform in normal use.
2 · Safe type conversion
Legacy stored a value loosely. It is converted defensively so a single bad row becomes empty instead of failing the entire table load.
3 · Value re-mapping
A dictionary value was renumbered or re-scaled in the new catalogue, so the value itself is translated in transit — not just carried.
4 · Orphan filtering
Legacy contains rows referencing records that no longer exist. They are excluded, because loading them would break the new platform's integrity rules.
Why the shape of these projections is checked before the run starts
Loading is done by position, not by column name — it is the fast bulk path. That means a projection that produces the right columns in the wrong order would load silently and put data in the wrong fields. So before any data moves, every projection is executed against the legacy database for its shape only and compared column by column with the target table. A mismatch stops the run at the pre-flight stage, not halfway through.
14. Table by table — the five business domains rebuilt separately
Content, enquiries, supervisor reports, inspection and payment are each rebuilt by their own procedure. They are handled apart from the application chain for one practical reason: legacy and new live on different database servers, and a rule that joins across servers row by row is unusably slow. So each domain runs in three moves — bring the new platform's reference tables next to the legacy data → apply the rules locally → send the finished result back.
14.1 Content library 12 output tables
| Output | Business logic applied |
|---|---|
| Cinema library | The cinema-works ledger, rebuilt onto the new content model. |
| Cinema languages | Film ↔ language, promoted to a proper many-to-many relationship (a film can be registered in several languages). |
| Video-games library | The games ledger. |
| Video-games languages | Game ↔ language, same many-to-many treatment. |
| Age-rating permits | Classification permits, linked to the work they classify. |
| Circulation media material | The circulating-media ledger. |
| Circulation media ↔ video games | A relationship the legacy system stored implicitly; it becomes an explicit link table so one media item can carry several games. |
| Poster / trailer permits | Promotional-material permits. |
| Book status history | Status history for books, newspapers, cinema works and games. The legacy system kept only the current status on the record; the history is reconstructed from the legacy action log so the new platform can show how each item reached its present state. |
| Newspaper status history | |
| Cinema status history | |
| Video-games status history |
14.2 Enquiries
Enquiry tickets, their detail lines and their action history are rebuilt against the new profile numbering. The heavy part is matching each ticket to the correct user profile — legacy holds a login id, the new platform holds a profile id, and where a user has several profiles the active one is preferred, then the oldest. Because that lookup runs for every ticket, the reference table is indexed on the staging side before the rules run.
14.3 Supervisor reports 1.5–2 h → minutes
Reports, their classifications and their notes. Three output tables, no business re-mapping beyond renumbering — this domain is listed mainly because it was the clearest performance case: the original version joined across servers for every report and took one and a half to two hours in UAT; rebuilt as a local, indexed join it now finishes in minutes. The output is identical.
14.4 Inspection 11 legacy tables → 34
The most heavily restructured domain. The legacy inspection module is not migrated as-is at all; it is re-expressed in the new, far more detailed model. Everything runs under a fixed system actor, so migrated records are clearly attributable to the migration rather than to a real inspector.
| Output | Business logic applied |
|---|---|
| Inspection tasks | The inspection visit itself, rebuilt onto the new task model. |
| Task attachments | Evidence files, re-pointed at the new file register. |
| Seized materials | What was confiscated during the visit. |
| Task contact persons | Who was present / responsible at the site. |
| Task timeline events | The visit timeline, reconstructed so the sequence of events survives. |
| Violations | Legacy violations are translated onto the new authoritative violation catalogue through three mapping tables: old violation sub-type → new violation item, old violation → catalogue item, and payment code → violation item. |
| Checklist items | The inspection checklist, which the legacy system did not model as a checklist. |
| Checklist violations | Which checklist line produced which violation. |
A dangling-reference case worth reading
Three legacy violation definitions had been deleted in the legacy system but were still mapped onto catalogue items 37, 38 and 39 — items that did not exist, because the catalogue only went up to 36. Forty-seven already-migrated violation rows were therefore pointing at nothing. Three new catalogue anchors were created and verified against the deleted definitions: identical wording, matching violation types. So this is a restoration, not a substitute. The rows remain flagged as coming from deleted definitions, because in the legacy system they genuinely were deleted — that fact is not rewritten.
Everything the procedure cannot resolve is written to a diagnostics table with an issue code, the source table and the source record — so no inspection record is dropped without a trace.
14.5 Payment 7 legacy tables → 15
| Output | Business logic applied |
|---|---|
| Transactions | The payment transaction, rebuilt against the new application numbering so every payment still points at the case it paid for. |
| Service application payments | The link between a payment and the service application, re-established on the new identifiers. |
| Payment receipts | Receipts, kept because they are financial evidence. |
| Magnati payment records | The payment-gateway records, preserved for reconciliation with the provider. |
| Penalty order links | A small link table (payment ↔ inspection visit) that reconnects fines to the inspection that issued them — in legacy this connection was implicit. |
Payment runs last of the five, because it consumes results produced by the earlier domains.
15. Table by table — finishing work after loading
A few things can only be corrected once the data is already in the new platform, because they depend on tables that arrive from different stages. These run at the end, against the new database, and every one of them can be re-run safely.
| Step | Tables touched | What it does and why it is last |
|---|---|---|
| Derive establishment type | Core.Establishments | The establishment type is filled from the owning profile's user type, following establishment → user link → profile. It cannot be done during loading, because the profiles it depends on arrive in a different stage and the legacy profile numbering is not the same as the new one. Where an establishment has several profiles, the primary (lowest-numbered) profile decides. Without this step the establishment detail page would be almost entirely blank, since the legacy column is empty for practically every record. |
| De-duplicate establishment partners | Core.EstablishmentPartners | Physically removes duplicate partner links and rows pointing at neither a person nor an establishment. The legacy application hid these by de-duplicating when reading; the new one reads plainly, so one establishment was showing five partners where the legacy screen showed one. The agreed position: dirty data is fixed by the migration, the application stays simple — neither codebase was changed. |
| Split contact numbers | 13 tables | Legacy stored a phone number as one free-text field; the new platform stores country code and local number separately. This fills the split columns across all thirteen tables that have them. It is done once, at the end, rather than inside each load path — those thirteen tables arrive through four different mechanisms, so splitting them in transit would mean four copies of the same algorithm, which would inevitably drift apart. Crucially, the same algorithm is byte-identical to the one used when assembling the application form content, so one legacy number always splits the same way in both places. |
| Re-point establishment emirate | Core.Establishments | Emirates were renumbered in the new dictionary; each establishment is re-pointed to the correct new emirate. |
| Re-seed identity gaps | All identity tables | Leaves a numbering gap after the highest migrated id, so that records created by users in the new platform can never collide with a migrated record. Safe to re-run. |
| FAHR integration data | Integration.* | Rebuilds the federal-HR integration payloads against the new identifiers, so the integration keeps working after cut-over. |
| Legacy file register | Common.StoredFiles | Legacy scattered file references across many tables as raw URLs. Every one of them is registered as a row in a single file register, with the filename normalised by exactly the same rule used in the application form content — which is what makes a document referenced on a migrated form actually download. |
Three copies of one rule, kept byte-identical on purpose
Two small rules — how a filename is extracted from a legacy URL, and how a phone number is split — each exist in more than one place, because they must run on different servers at different stages. They are maintained as literally the same text. If one copy is edited and the others are not, the same phone number would be split two different ways, or a document on a migrated form would stop matching its file in the register. This is written into the code as an explicit contract.
UMC / NMA data migration — business guide. Prepared from the migration handbook set; describes the behaviour of the current migration programme.