A fintech data platform, built solo

Founding Senior Data Engineer at Finally, July 2025 to September 2026. Three products on one Snowflake warehouse: a corporate card program and a bookkeeping product across two generations.

What I inherited

A repo of 281 models, every one of them copying a Postgres table into Snowflake. No marts, no dimensions, no facts, no tests, no documentation, and nobody in the seat; the platform engineer who had stood up the replication left two months before I started. The hardest source was the card platform, which gives every customer their own Postgres schema, so the replication layer fanned every table out across roughly 1,600 tenant schemas of about 120 tables each.

The shape of it

Sources flow through bronze, silver, and gold layers to dashboards, lender tapes, a sponsor-bank pack, and the AI service. Airflow and Snowflake tasks orchestrate the layers; row access and masking policies cover silver and gold.Card platformschema per tenantBookkeepingAurora, two generationsAI telemetryper-call eventsFivetran, then self-hosted AirbyteBronzeraw landing, 196K objectsSilverflattened, tenant-normalized, 1.4K modelsGoldDims, facts, martsboard metricsReportingtapes and packsML eval and featurescorpus, shadow runsDashboardsSuperset and Streamlitexec, finance, riskLender and banktasks, S3, Lambda, SQSGPG over SFTPAI servicefeature aggregatesas low-trust priorsAirflow on MWAA, Snowflake tasksRow access and masking438 dbt models across the three layers, 326 tests, 4 SCD-2 snapshots,75 native Snowflake procedures, tasks, and report functions, 7 DAGs, 2 Lambdas.About 280M rows resident; 10.6B rows materialized a year.

The build, in order

The arc matters more than any one piece: the analytics layer first, then the orchestration and ingestion underneath it once the vendors had to go, then governance and model evaluation on top once the company's AI products started handling customer financial text.

  1. Jul to Nov 2025The gold layer, product by product: card usage and activation cohorts, the sales funnel, risk and delinquency, then platform-wide revenue and retention. Tests shipped with each model, not after.
  2. Dec 2025The delinquency model behind the lender covenant rewritten from five recursive CTEs to window functions. Six hours on an X-Large became thirteen seconds on an X-Small. In the card platform's backend, a Celery task that registers each new tenant with the Fivetran API on creation, naming the schema and only the tables the warehouse reads, with exponential backoff on retry. Before it, a new tenant's whole schema synced, every table.
  3. Jan 2026The borrowing-base recovery: companies missing from the lender's loan tape traced to spreadsheet and backend data-integrity defects, fixed upstream after backend query approval, roughly half a million dollars of capacity back.
  4. Feb 2026Tenant-flattening fan-out pruned from 92 models to the 9 anything read. Pipeline queries fell from 13 million a month to under 2 million, and stayed there. Observability in the same month: Slack and email alerting on tapes, DAGs, and connectors, and a Streamlit dashboard over a dbt Cloud API ingestion pipeline landing in a vendor-data bronze database, mapping dependencies, refresh intervals, and schedule collisions.
  5. Apr 2026The retrieval architecture and corpus design for a customer-facing RAG assistant, written as the forcing function for a product nobody had scoped yet.
  6. Apr to Jun 2026Two vendor exits without an outage: dbt Cloud to Airflow on MWAA provisioned in Terraform, and four sources from Fivetran to self-hosted Airbyte with the VPC peerings adopted into state.
  7. Jun to Sep 2026Governance for the AI surfaces, the classifier evaluation platform, and per-call cost and adoption analytics.

Decisions I would defend

Model to consumption, not to completeness
The replication layer brought everything across in case someone needed it. Ninety-two models fanned a UNION across about 120 tenant schemas every night to serve nine tables anyone read. The fix was a config block disabling the rest, and the diff was almost entirely deletions. If I started again I would land nothing until something downstream asked for it.
Windows over recursion
A recursive CTE for a running total or a forward-fill is a tell. Every recursion in the delinquency model became a window function over a shared date spine, and the result was measurable in query history: 617,770 rows in 12.7 seconds worst case, zero spill. The legacy view now carries a do-not-use prefix, which is the part I find most satisfying.
Every definition lives in exactly one place
The paying-customer universe is versioned and enforced identically in the company dimension and the board segmentation mart, so the two cannot disagree. One macro defines the evaluation denominator for every ML metric, so offline and online scoring cannot drift apart. If two tables can disagree about a definition, eventually they will, and you find out in a board meeting.
Point-in-time, decided before it is expensive
Any time a mutable attribute joins a historical fact, ask whether today's value is about to rewrite last quarter. Three SCD-2 snapshots exist for that reason: grace periods, because the delinquency model rebuilds all history nightly; business status, so month-end counts use the status in effect; loyalty balances, because the source is current-state only.
Tenant isolation as a design input
Every silver and gold table is keyed and clustered on the tenant so the row access policy prunes partitions instead of scanning. The policy checks role membership against an entitlements table, since a policy cannot trust session variables anyone can set. Masking is split by job: the eval reader sees free text and not dollar values. Because these tables are rebuilt with create-or-replace, each model re-attaches its own policy in a post-hook, and the proposal states in writing that the raw landing table is not covered.
Schedule on data, not on clocks
Gold DAGs run when the silver DAG publishes an Airflow Dataset, not at a fixed hour, so gold rebuilds the moment silver lands and never on stale silver. Dataset URIs are declared once in a shared module, because Airflow matches on the string and a drifted literal fails silently.
Test the invariant, not the row count
A not-null test says the pipeline ran. A monotonicity test says the business logic is still right: days past due go up by exactly one or reset to zero. Ending balance equals beginning plus spend minus payments, to the cent. Recoveries never exceed the write-off they net against. Approved company counts never decrease. Six of those sit beside 326 generic tests, and they are the half that matters.
Build order is a dependency
The delinquency model was one day short and the SQL was fine. Its date spine filtered on a pre-computed "is past" flag from a dimension that rebuilt after it in the schedule, so yesterday was never past. The fix was a run-time cutoff. The lesson became a dependency-review checklist the repo now reviews every model change against: refresh cadence, build order, downstream blast radius, and never a pre-computed relative-date flag as a spine filter.
Write down what a metric does not measure
One release criterion was no class recall drop over fifteen points. The eval mart's class-concentration columns look like they cover it and do not; they catch collapse to one class and miss one class's recall falling while the distribution holds. The limitation is in the model header, the columns are labeled a collapse canary, and the follow-up mart at the right grain is specified.
The Terraform apply I did not run
Adopting eight live VPC peerings into state, the plan also showed drift in another team's module: a different node type than the code declared, a public load balancer where the code said private. Applying would have replaced their node group and taken down ingestion. I merged the no-op, did not apply, wrote up the specific resources at risk, and took it to the team that owned them. When you are the only person in your function, the temptation is to just fix it. The right move was to make the risk legible.

What it cost to run

Pipeline compute is the Snowflake credits burned rebuilding models on the warehouse the dbt service account uses, nothing else. Both series are from Snowflake's own account usage views, 2026.

MonthPipeline creditsPipeline queries
Jan1,35213.4M
Feb1,1639M
Mar1,1651.9M
Apr1,2291.8M
May8671.2M
Jun2730.08M
Jul3440.25M
Aug3310.25M

The query collapse in March tracks the February pruning commit exactly. The credit drop in June tracks the vendor exits. A company restructuring in April reduced transaction volume in the same window, so part of the credit decline is organic. Pipeline compute is driven by model count and rebuild size far more than by customer volume, which is why the pruning and orchestration work is the dominant lever, but the fully attributable figures are the query series and the delinquency rebuild, measured on the same model and warehouse before and after.

Boundaries

Things a reference check would turn up, so they are here first.