Systems architecture · End-to-end data platform

From four source databases to one governed BI layer

Both TMS systems, Sage Intacct, and the maintenance system feed the warehouse through SnapLogic pipelines that Prefect orchestrates; Qlik Sense sits on top of the conformed model and carries intelligence out to the organization. The ETL demo below walks through one of those pipelines in detail.

Synthetic demonstration data
The stack at a glance

Solid arrows carry data left to right; dashed lines are Prefect's control plane.

TMS1 — LegacyRoadSQL Server · legacyTMS2 — NexHaulcloud TMS · SORSage Intacctfinancial ERPMaintenanceshop work ordersSnapLogicdata pipelinesDataWarehousestaging → martsQlik Sensegoverned BIPrefectorchestrationschedules · retries · DQ gates · reload triggersExecutivesOperationsFinance & AROrg-wide reportsthe organization
  • Source database
  • SnapLogic pipelines
  • Data warehouse
  • Qlik Sense
  • Prefect orchestration
  • Data flow
  • Control / scheduling

Layer detail

The same stack, expanded: what each layer contributes and how responsibility is divided between the pipeline tool, the orchestrator, the warehouse, and the BI layer.

Source databases

  • TMS1 — LegacyRoadlegacy

    On-prem SQL Server · dispatch & billing (sunsetting)

  • TMS2 — NexHaul Cloud

    Cloud TMS · system of record for operations

  • Sage Intacct

    Financial ERP · GL, AP/AR, invoicing

  • Maintenance System

    Shop work orders, parts, unit downtime

Integration & orchestration

SnapLogic — data pipelines

Extract from each source · conform field names and data types to the warehouse model · merge overlapping TMS1/TMS2 entities · land to staging

Prefect — orchestration

Schedules and dependencies across every pipeline · retries and failure alerting · data-quality gates before promotion · run lineage and audit logs

Storage & modeling

Data Warehouse

Staging (source-shaped) → conformed dimensional model (FactLoad, FactInvoice, shared dimensions) → subject-area marts. One auditable home for every load, invoice, and work order.

Business intelligence

Qlik Sense

Governed apps on the warehouse marts — associative self-service analysis, section access for row-level security, reload tasks triggered after Prefect marks loads green.

Dissemination to the organization

  • Executive dashboards

    Margin, utilization, service KPIs

  • Operations desk

    Late-load & exception alerts

  • Finance & AR

    Invoice aging, cost reconciliation

  • Org-wide distribution

    Scheduled reports & embedded views

Reading the flow: SnapLogic owns how data moves and reshapes; Prefect owns when it moves and whether it's trustworthy; the warehouse owns the single conformed model; Qlik Sense never reads a source system directly — every dashboard traces back through the governed path.

Technical demo · ETL pipeline construction

Migrating a legacy TMS without breaking the warehouse

Two transportation management systems run in parallel during a migration: the legacy on-prem system (TMS1) and its cloud replacement (TMS2). The same entities live in both with different names, formats, and types. Pick a table to compare the two models, see the field mapping, and read the Prefect flow that unifies them nightly.

  1. SQL Server (SSMS)Extract dbo.* via pyodbc
  2. PrefectOrchestration, retries, logging
  3. SnapLogicRename fields · cast types
  4. MergeUnion + dedupe on natural key
  5. WarehouseStaged, transactional load

Migration table

Loads dispatched and delivered. TMS1 stores dates and money as text, city and region in one column, and status as a numeric code.

TMS1 · LEGACY

LegacyRoad — on-prem SQL Server

dbo.Shipments · top 10 rows

ShipmentIDintShip_Dtvarchar(10)Del_Dtvarchar(10)Origvarchar(60)Destvarchar(60)Cust_Cdvarchar(8)Equip_Cdchar(2)XBorderchar(1)Ld_Milesvarchar(8)Tot_Revvarchar(12)Stssmallint
1024106/29/202607/01/2026TORONTO, ONCHICAGO, ILNGRG01DVY848$2,481.604
1024206/29/202606/30/2026CHICAGO, ILWINNIPEG, MBPFCO02RFY901$3,612.754
1024306/30/202607/02/2026DALLAS, TXCHICAGO, ILNGRG01DVN972$2,355.103
1024406/30/202607/01/2026LAREDO, TXMONTERREY, MXTBMT05FBY244$1,163.204
1024507/01/202607/01/2026TORONTO, ONMONTREAL, QCLKBV03DVN352$989.453
1024607/01/202607/03/2026CALGARY, ABDENVER, COGPAS08RFY1104$4,020.002
1024707/02/202607/03/2026TORONTO, ONDETROIT, MIDMAC04DVY236$1,041.303
1024807/02/202607/04/2026DALLAS, TXATLANTA, GAMWBP06FBN791$2,196.852
1024907/03/202607/05/2026MONTREAL, QCTORONTO, ONCSPP07DVN349$918.201
1025007/03/202607/06/2026MONTERREY, MXLAREDO, TXDMAC04DVY251$1,205.001
TMS2 · TARGET

NexHaul Cloud — target data model

tms2.shipments · top 10 rows

shipment_idbigintshipment_numbervarchar(12)pickup_datedatedelivery_datedateorigin_cityvarchar(40)origin_regionchar(2)dest_cityvarchar(40)dest_regionchar(2)client_codevarchar(8)equipment_typevarchar(10)cross_borderbooleanloaded_milesinttotal_revenuedecimal(12,2)statusvarchar(12)
20411SH-204112026-07-062026-07-08ChicagoILTorontoONNGRG01dry_vantrue8352464.2delivered
20412SH-204122026-07-062026-07-07TorontoONMontrealQCLKBV03dry_vanfalse346972.4delivered
20413SH-204132026-07-062026-07-08ChicagoILWinnipegMBPFCO02reefertrue8873548.1in_transit
20414SH-204142026-07-072026-07-08LaredoTXMonterreyMXTBMT05flatbedtrue2491187.5in_transit
20415SH-204152026-07-072026-07-09DenverCOCalgaryABGPAS08reefertrue10923961.75in_transit
20416SH-204162026-07-072026-07-09AtlantaGAChicagoILMWBP06flatbedfalse7242011.3planned
20417SH-204172026-07-082026-07-09TorontoONDetroitMIDMAC04dry_vantrue2331028.6planned
20418SH-204182026-07-082026-07-10DallasTXLaredoTXGPAS08dry_vanfalse4411112.9planned
20419SH-204192026-07-082026-07-10ChicagoILDallasTXNGRG01dry_vanfalse9682340.55planned
20420SH-204202026-07-092026-07-11MontrealQCTorontoONCSPP07dry_vanfalse351931planned

Field mapping — applied inside the SnapLogic pipeline

Dedupe key after conforming: shipment_number (TMS2 wins collisions — it is the system of record for migrated entities)

TMS1 fieldTMS1 typeTMS2 fieldTMS2 typeTransform
ShipmentIDintshipment_numbervarchar(12)Prefix legacy key → 'LR-' + id (keeps both systems unique)
Ship_Dtvarchar(10)pickup_datedateParse MM/DD/YYYY → ISO 8601 date
Del_Dtvarchar(10)delivery_datedateParse MM/DD/YYYY → ISO 8601 date
Origvarchar(60)origin_city / origin_regionvarchar(40) / char(2)Split on ',' · title-case city · trim region
Destvarchar(60)dest_city / dest_regionvarchar(40) / char(2)Split on ',' · title-case city · trim region
Cust_Cdvarchar(8)client_codevarchar(8)Rename · uppercase · trim
Equip_Cdchar(2)equipment_typevarchar(10)Code map: DV→dry_van · RF→reefer · FB→flatbed
XBorderchar(1)cross_borderbooleanY/N → true/false
Ld_Milesvarchar(8)loaded_milesintCast text → int
Tot_Revvarchar(12)total_revenuedecimal(12,2)Strip '$' and ',' → decimal(12,2)
Stssmallintstatusvarchar(12)Code map: 1→planned · 2→in_transit · 3→delivered · 4→invoiced

Orchestration — Prefect flow for this table

The flow below updates with the selected table. Extraction pulls the raw legacy rows untouched; the SnapLogic triggered task owns every rename and cast (validated against an explicit schema contract); the merge dedupes on the natural key with TMS2 as system of record; the load stages and swaps inside one transaction.

flows/migrate_shipments.py

"""
Migration flow — Shipments: LegacyRoad (TMS1) -> NexHaul (TMS2) -> warehouse.

Extract dbo.Shipments from the on-prem SQL Server behind SSMS, conform it
to the TMS2 data model through a SnapLogic pipeline (field renames + type
casts live there, version-controlled with the pipeline), merge with native
TMS2 records, and load the unified table to the warehouse.
"""

import io
import os

import pandas as pd
import pyodbc
import requests
from prefect import flow, task, get_run_logger
from prefect.blocks.system import Secret

SOURCE_TABLE = "dbo.Shipments"                # TMS1, SQL Server (SSMS)
TMS2_TABLE = "tms2.shipments"                  # TMS2, already conformed
WAREHOUSE_TABLE = "dw.fact_shipment"        # load target
NATURAL_KEY = "shipment_number"          # dedupe key after conforming
SNAPLOGIC_TASK_URL = (
    "https://elastic.snaplogic.com/api/1/rest/slsched/feed/FreightIntel"
    "/migration/shipments_conform_task"
)

# Contract with the SnapLogic pipeline: exact columns + types it must emit.
EXPECTED_SCHEMA = {
    "shipment_id": "bigint",
    "shipment_number": "varchar(12)",
    "pickup_date": "date",
    "delivery_date": "date",
    "origin_city": "varchar(40)",
    "origin_region": "char(2)",
    "dest_city": "varchar(40)",
    "dest_region": "char(2)",
    "client_code": "varchar(8)",
    "equipment_type": "varchar(10)",
    "cross_border": "boolean",
    "loaded_miles": "int",
    "total_revenue": "decimal(12,2)",
    "status": "varchar(12)",
}


def sqlserver_conn(dsn_secret: str) -> pyodbc.Connection:
    """Connection via a DSN stored in a Prefect Secret block (no creds in code)."""
    return pyodbc.connect(Secret.load(dsn_secret).get(), timeout=30)


@task(retries=3, retry_delay_seconds=60)
def extract_tms1() -> pd.DataFrame:
    """Pull the legacy table exactly as stored — no transforms at extract time."""
    logger = get_run_logger()
    with sqlserver_conn("tms1-ssms-dsn") as conn:
        df = pd.read_sql(f"SELECT * FROM {SOURCE_TABLE};", conn)
    logger.info("TMS1 extract: %s rows from %s", len(df), SOURCE_TABLE)
    return df


@task(retries=3, retry_delay_seconds=120)
def conform_via_snaplogic(tms1_df: pd.DataFrame) -> pd.DataFrame:
    """
    POST the raw TMS1 rows to the SnapLogic triggered task that owns the
    field-name and data-type mapping, and read back TMS2-shaped records.
    """
    logger = get_run_logger()
    response = requests.post(
        SNAPLOGIC_TASK_URL,
        params={"bearer_token": Secret.load("snaplogic-token").get()},
        json=tms1_df.to_dict(orient="records"),
        timeout=600,
    )
    response.raise_for_status()
    conformed = pd.DataFrame(response.json())

    missing = set(EXPECTED_SCHEMA) - set(conformed.columns)
    if missing:
        raise ValueError(f"SnapLogic output missing columns: {sorted(missing)}")
    logger.info("SnapLogic conformed %s rows to the TMS2 model", len(conformed))
    return conformed


@task(retries=3, retry_delay_seconds=60)
def extract_tms2() -> pd.DataFrame:
    """Native TMS2 records — already in the target model."""
    logger = get_run_logger()
    with sqlserver_conn("tms2-replica-dsn") as conn:
        df = pd.read_sql(f"SELECT * FROM {TMS2_TABLE};", conn)
    logger.info("TMS2 extract: %s rows from %s", len(df), TMS2_TABLE)
    return df


@task
def merge_systems(conformed_tms1: pd.DataFrame, tms2: pd.DataFrame) -> pd.DataFrame:
    """
    Union both systems on the shared model, then dedupe on the natural key.
    TMS2 wins collisions: it is the system of record for migrated entities.
    """
    logger = get_run_logger()
    merged = pd.concat(
        [conformed_tms1.assign(source_system="TMS1"), tms2.assign(source_system="TMS2")],
        ignore_index=True,
    )
    before = len(merged)
    merged = merged.sort_values("source_system").drop_duplicates(NATURAL_KEY, keep="last")
    logger.info("Merged %s rows -> %s after dedupe on %s", before, len(merged), NATURAL_KEY)
    return merged


@task(retries=2, retry_delay_seconds=300)
def load_warehouse(df: pd.DataFrame) -> int:
    """
    Bulk-load to a staging table, then swap into the target inside one
    transaction so downstream reports never see a half-loaded table.
    """
    logger = get_run_logger()
    staging = f"{WAREHOUSE_TABLE}_stg"
    with sqlserver_conn("warehouse-dsn") as conn:
        cursor = conn.cursor()
        cursor.fast_executemany = True
        cursor.execute(f"TRUNCATE TABLE {staging};")
        placeholders = ", ".join("?" for _ in df.columns)
        cursor.executemany(
            f"INSERT INTO {staging} ({', '.join(df.columns)}) VALUES ({placeholders})",
            df.itertuples(index=False, name=None),
        )
        cursor.execute(
            f"BEGIN TRAN; TRUNCATE TABLE {WAREHOUSE_TABLE}; "
            f"INSERT INTO {WAREHOUSE_TABLE} SELECT * FROM {staging}; COMMIT;"
        )
        conn.commit()
    logger.info("Loaded %s rows into %s", len(df), WAREHOUSE_TABLE)
    return len(df)


@flow(name="migrate-shipments", log_prints=True)
def migrate_shipments() -> None:
    tms1_raw = extract_tms1()
    conformed = conform_via_snaplogic(tms1_raw)
    tms2_native = extract_tms2()
    unified = merge_systems(conformed, tms2_native)
    load_warehouse(unified)


if __name__ == "__main__":
    # Local run; in production this deploys on a schedule via prefect.yaml.
    migrate_shipments()