Prashant Vithani

Prashant Vithani

Data infrastructure & compilers · Bengaluru, India · hello@prashantvithani.com · github.com/prvithani

A decade building the ingestion and computation layer of a marketing analytics platform spanning 80+ advertising and analytics sources. One instinct, applied at rising levels of abstraction and each time forced by the failure of the level before it: move behaviour out of imperative code into a declarative artifact, then build the compiler that turns it into an executable plan.

PeriodWhat is declaredWhat compiles it, into whatShipped
2026an EDN dataflow program11 pure passes → content-addressed task DAGnot in production
2025a channel definitiondefinition → schemas + pipeline + joins + annotationno — shelved at MVP
2022–25a channel configurationconfig → Apache Spark planyes, full coverage
2021a formal definition of precomputationspecification → libraryyes
2019–20a DAG of tagging rulesrule-graph version → deterministic per-object annotation planyes
2016–18a customer's metric formulaexpression AST → MongoDB aggregation pipelineyes

Clarisights (formerly AdWyze) — 2015–present

Staff Engineer, Data Infrastructure. The company runs without a formal IC ladder; the title reflects the scope held: built the data plane and led it under one team. De-facto lead of the ETL team with one peer, 2021–22; Tech & Team Lead of the ingestion team (5 engineers), May 2023 – Feb 2025; project lead since, by request.

Mojart — declarative pipeline compiler, Clojure

author · project lead · 2026
  • Purpose: make adding or changing a data pipeline an act of authoring a spec rather than writing pipeline code, and keep changing one afterwards cheap and safe. Content identity so an edit costs only what changed — a platform where every schema change means a full recompute stops changing, and ossifies; a run-ledger so failures resume rather than restart; an atomic generational swap so downstream consumers never see a schema shift or a half-published dataset. Mojart is the primary write path from raw ingestion to the semantic layer.
  • Derived a tensor model for metrics with multi-dimensional variants: ~30 source events explode into 800–1,000+ concrete metrics across axes (attribution window, type, cohort period…); each metric is a point in that space, a scalar is the degenerate tensor, and a coalesce/broadcast rule makes ratios of mixed-rank operands well-defined — so the DSL declares the space generatively instead of enumerating columns, resolved by a fixed-point loop over the column registry.
  • Built the metadata layer that publishes each dataset as a Model — typed columns, grain, tensor-valued measures — the contract the semantic layer binds to a shared vocabulary, resolving a widget's query to the right tables and grains with no producer renames and no redeploy.
  • A pure, content-addressed compiler: an EDN program through 11 passes into a task DAG carrying a Merkle content-identity per node and per field. Deploy diffs that identity against the last committed generation manifest and swaps atomically; the runtime reconciles a per-cell ledger so only dirty (artifact × partition) cells rebuild — dirty meaning missing, definition-changed, or built from an older version of the source data, which the ledger records alongside the identity.
  • The runtime: every task is a DBSP-style circuit with a checkpoint feedback edge — cursor, output and a page-domain fingerprint saved per page, crash-resume with a monotone undo, four paging axes (rowid, hash-bucket, branch-rowid, keyset), page-parallelism only for circuits that declare thread-safety. Declared persist steps become materialised, versioned tables that downstream work resumes from.
  • Merged and running end to end pre-production — not in production. Backfilled two years of a customer's data, 88M rows, in roughly one hour after executor optimisations (the first recorded run took ~225 minutes).
  • Raised checkpoint write throughput from 2.70 to 10.58M rows/min (3.9×) at 100M rows by sweeping write-ahead-log budget against merge concurrency and showing concurrency is the dominant, memory-cheap lever — on a colleague's storage engine.

Mu — distributed ingestion, Scala/Spark

designed and built from scratch · principal author · 2022–2025
  • Designed and built it from scratch as the replacement for two systems: a Ruby ingestion framework grown inside the Rails monolith, and the Go/Apache Drill service beside it that existed to query flat files the Ruby side couldn't. One general-purpose ingestion layer in their place, where a channel is a configuration compiled into a distributed plan rather than a codepath — so adding a source stopped being an engineering project. Mu's rollout drove Drill's usage to zero; I later deleted the cluster and its front-end service.
  • Took a 5M-row weekly customer ingest from ~1.5 days to ~1.5 hours. The old duration structurally could not meet a daily data-availability deadline. Achieved by batching dereference at 1,000 rows per request across 32 Spark executors, and reading Parquet and BigQuery natively instead of transcoding to CSV so filters push down to the source. A 500k-row daily job went 1 hour → 10 minutes on the same change. Verified rather than asserted: ran a live channel and a clone through both pipelines and compared rendered output before merging.
  • Reached 100% of the platform's customer-data and custom-analytics channels; the predecessor was retired. This was the objective I set and owned.
  • Still the ingestion path four years on. On 31-day production means: ~680M rows/day (peak day 1.33B; peak hour 120M), ~8,900 jobs/day with 73% in a six-hour window (peak 1,889/h, 7× the mean), with the 12 workers at 74% occupancy through the wave and ~2 busy otherwise — a demand-bound figure against a next-morning deadline.
  • Benchmarked the per-row validation path (deserialise, type-cast, date-parse, write Parquet) over a 4.76B-row / 2.2 TB source: 52.1M rows in 3.2 minutes on 16 cores, 8 GB heap — ~16M rows/minute with date parsing on every row.
  • Compiles a channel config into a Spark plan across 11 source types, 10 processors and 3 sinks. A team system — largest single author, not the only contributor.
  • 2024: brought a 17M-object/day feed inside a 4-hour job SLA — derived the per-request budget (~6 s vs a 20 s median), found a 60-s stall in a metrics thread, batched cache reads, moved payload parsing off the web framework (~700 ms × 17,500 requests). Dereference from timeouts to 53 minutes; over the next ten months mean job time 140 → 54 min while the feed grew 8 → 12M rows/day (peak 21.7M); annotation object sync 1,100 → 6,700 objects/s.
  • Ported metric annotation from the OLTP stores to the in-house OLAP store: the heaviest channel 15 h → 4 min (with a colleague's server-side fix).

A rule I earned the hard way

2024–2025

Two account-identifier migrations I led in late 2024 and early 2025 duplicated data for several customers; the second took ten days to detect. I owned both publicly and wrote the root-cause analyses. What came out of them: duplicate-account detection that now runs in production, and a standing rule for any destructive change — a named second reviewer, written success criteria, and a detection query before the first row is touched.

Per-object annotation

co-designed · led delivery · 2019–2020
  • Wrote the v2 specification after an incident review found the tagging system had no predictable failure rate and no way to assert its own sanity. It names the properties the system must have — determinism, atomicity, eventual causal consistency, isolation, visibility, checkpointing, fault tolerance — then evaluates three candidate architectures against a failure-mode analysis and selects one.
  • Built the metadata service that versions the rule graph, so a tag can be attributed to a specific rule-graph version across a hierarchy of objects; implemented the PostgreSQL-based work executor, and the JIT-compiled Ruby evaluator — rules compiled to generated code rather than interpreted one by one. A colleague added the C-codegen sibling and later optimised the Ruby path further.
  • Validated the design where it was most likely to break and published the results to the team (May 2020). The architecture put Postgres in the critical path as the work executor, so I measured that component rather than the pipeline: 17–19,000 objects/sec, up from 2–3,000 once I isolated a single Postgres planner setting costing 6–8× throughput; and the split-finalisation query cut from 20.5 minutes to 111 seconds with throughput flat either side, so the gain was attributable to the query, not the environment. Held disk health as a controlled variable so a failing disk could not read as a regression.
  • Years later, as team lead, I set the engine's performance objective — full re-tag under twelve hours for every channel after a customer edits a rule. The engine holds ~4.7 billion objects across 217 customer schemas and rewrites ~12% of them daily (~545M updates + 37M inserts); the largest channel, ~440M objects, re-tagged in ~1 h median, 2–5 h p90 across the whole pipeline — rule evaluation, tagging and persistence, not just the executor. Met September 2024 and beaten on the largest channels. The annotation team built the mechanism and took the measurements; the target and the accountability were mine.
  • The team wrote most of the implementation; the design, the direction and that benchmark were mine. Still the platform's annotation engine six years on.

Earlier

2016–2021
  • Derived-metrics compiler — principal author. Lowers a customer-authored formula, via its expression tree, into a MongoDB aggregation expression, recursively inlining nested derived metrics so the emitted query touches only base metrics. Extended the language with conditional metrics and dimensions; contributed operator introspection back to the expression library upstream. Still in production.
  • Dataframe migration — a colleague proposed Polars; I produced the benchmark that settled it (150× time, 73,000× memory) and, when it was challenged, re-ran it at 100× the data where the incumbent was OOM-killed. Forked the gem to restore support for the Ruby version we were pinned to, so it could actually be installed. The team migrated the workloads.
  • Retired an Apache Drill cluster — set its removal as a team objective, moved Bing reporting and CSV aggregation in-process onto Polars, deleted the Go service in front of it and drove the Kubernetes teardown. Freed a dedicated non-preemptible four-node pool holding 16 vCPU and ~107 GiB reserved around the clock. The service existed because of a recommendation I had made five years earlier.
  • Precomputation — sole author of the 2021 formal specification (glossary, row definition, unit-of-work/runner/scheduler/sink). It was never built in Ruby; its vocabulary and replacement semantics became the design basis of Mu the following year.
  • File-channel import caching (2021) — the pipeline Mu replaced: 4.5M rows/day from 17–18 h to ~8 h by lifting campaign lookups into a cache (0 → 78% hit rate); the account-cache half was a colleague's.
  • Channel integrations — Criteo, AppNexus, The Trade Desk, Spotify, Adjust, Bing, GA4, Snowflake, Databricks and others, plus the cross-channel analytics metric layer.

Leadership

  • Tech & Team Lead of the ingestion team, 5 engineers, May 2023 – Feb 2025; de-facto lead of the ETL team with one peer before that. Wrote the team's performance reviews and ran hiring loops; started and ran the engineering on-call rotation (2020) — its norms as well as its roster.
  • Asked for, and got, the whole data plane under one umbrella — ingestion, the annotation engine, dereferencing and the custom-channel stack owned by one team — and led that team, so interconnected parts stopped being negotiated across org lines.
  • Integrated the data plane with the rest of the product and the control plane, working with cross-functional stakeholders: the ingestion service and the annotation engine wired into the orchestration layer for execution and visibility, and the serving side fed from one contract.
  • Initiated the consolidation of fragmented customer workloads onto one config-driven service, cutting the operational burden on the people running them and making the experience uniform.
  • Migrated more workloads onto the unified pipeline at every opportunity, retiring non-essential, under-performing systems as their users moved — the Apache Drill cluster and its Go front-end, the Kafka consumers, per-source Ruby jobs — removing debt and their infrastructure cost with them.
  • Refused committed capacity in public and in writing, naming the trade-off each time. Argued that a roadmap should carry problems, with investigation as an action item beneath them. Drove a company-wide migration of team communication tooling and have run the server since.
  • Stepped back from the lead role in 2025 at my own request, after a period of sustained overtime, to found the next system as an individual contributor; the team I handed over still ships the pipeline I started.

A reversed decision

I led a platform-wide MRI → JRuby migration in 2018 for thread parallelism under an expression evaluator that was timing out. It measured ~25% faster on the benchmarked query and roughly halved CPU-heavy job durations, and it still failed — fixed JVM allocation against Kubernetes' soft limits caused OOM kills, the GIL had been hiding unsafe cross-thread ActiveRecord use, the test suite tripled, and JVM warmup made releases visibly slow. Reverted after six months. It produced merged upstream fixes to JRuby, activerecord-jdbc-adapter, rake-compiler and concurrent-ruby, and one rule I have applied ever since: instrument first, then decide. The full write-up is here.

Stack · Scala/Spark · Clojure · Ruby/Rails · Go · Elixir · Python · PostgreSQL · ClickHouse · DuckDB · MongoDB · Kafka · Airflow · Kubernetes · GCP