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
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
| Month | Pipeline credits | Pipeline queries |
|---|---|---|
| Jan | 1,352 | 13.4M |
| Feb | 1,163 | 9M |
| Mar | 1,165 | 1.9M |
| Apr | 1,229 | 1.8M |
| May | 867 | 1.2M |
| Jun | 273 | 0.08M |
| Jul | 344 | 0.25M |
| Aug | 331 | 0.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.
- I did not build the RAG serving service: the agent, the vector store, and its calibration harness belong to two colleagues. I authored the retrieval architecture and corpus design, built the classifier evaluation platform, the cost and adoption analytics, and the tenant governance model, and wrote the architecture recon of their service that the eval data contract was specced from.
- The AI analytics models shipped merged but disabled, pending the upstream event source. Built, reviewed, and lineage-validated; not yet producing rows when I left.
- The AWS side of the lender delivery pipeline predates me and has no infrastructure as code: the queues, notifications, and roles were console-built. I documented the whole path, including that.
- The platform-metrics domain had a colleague leading it. My share there is 19 of 64 commits. Every other domain on this page is mine by a wide margin, and the Airflow and governance repos are mine entirely.