work project
Enterprise Data Integration Platform
A multi-tenant Kubernetes control plane and a low-code pipeline builder, shipped as one product.
- Go
- Python
- FastAPI
- React
- TypeScript
- Apache Airflow
- Kubernetes / Helm
- ArgoCD
- Terraform
- Docker
- GitHub Actions
- Architected a Go control plane that provisions an isolated Kubernetes environment per tenant — hours of manual setup down to minutes — with RBAC, quotas, and GitOps reconciliation per namespace.
- Built the 40+ reusable component library new pipelines are composed from instead of hand-written glue, sustaining 2M+ records daily through a checkpointed execution engine.
- Shipped GDPR-compliant privacy controls — PII detection and configurable anonymization across 100k+ customer records.
What it was
FlowGenX sold a data platform to teams who wanted production pipelines without hiring a data-engineering department. It had two halves that had to behave like one product: a control plane that stood up an isolated Kubernetes environment for each tenant, and a low-code editor where someone who was not an engineer could wire a pipeline out of pre-built components and run it against real data.
What made it hard
Standing up a tenant environment was a six-to-eight hour job of a human reading a runbook and pasting YAML. That was the headline problem, but the harder one sat underneath it: every tenant wanted isolation guarantees that imply a dedicated cluster, and nobody wanted to pay for a dedicated cluster. On top of that, the pipelines were long-running. A job that takes four hours will meet a node eviction, a rate limit, and a malformed record, and it has to survive all three without a human babysitting it. And because customers were pushing customer data through it, PII handling could not be a feature added in the second year.
What I owned
I architected and wrote the provisioning service in Go, including the resource-templating layer, RBAC wiring, and the tenant quota model. I led the workflow editor and the execution engine behind it, and built out the component library the editor draws from. I designed the state management for long-running runs and implemented the privacy and anonymization components. Infrastructure delivery — the CI/CD and monitoring plumbing — was a joint effort with DevOps rather than something I owned alone.
The decision I would defend
Namespace-per-tenant, not cluster-per-tenant. Every tenant gets a namespace with hard resource quotas, network policies, and its own RBAC boundary, all reconciled by ArgoCD from a declarative spec.
The cost is real and I would not pretend otherwise. A shared control plane is a shared blast radius: a kubelet CVE is everyone’s problem, and a tenant that saturates a node degrades its neighbours in ways quotas do not fully prevent. What it bought was the entire premise of the product. Provisioning became a reconcile loop instead of a cluster build, which is the difference between minutes and hours, and the infrastructure bill stayed proportional to actual usage rather than to tenant count.
Where it landed
Provisioning went from most of a working day to minutes. The component library reached 40+ pieces and the platform sustained 2M+ records daily through the execution engine. The privacy layer covered PII across 100k+ customer records with configurable anonymization strategies.
What I would do differently
I picked the wrong granularity for the components. One component to one operation felt clean to me because that is how an engineer decomposes a problem, but the people actually using the editor were analysts, and they ended up building sprawling graphs to express what a single SQL statement says in four lines. Those graphs were genuinely harder to read and debug than the code they replaced, which defeats the point. I would ship fewer, fatter, task-shaped components and put a raw SQL and Python escape hatch in front of users from the first release rather than treating it as an admission of defeat.
The second thing is checkpointing. I designed it as an internal reliability mechanism, so when a run resumed after a failure, operators had no way to see what the earlier attempt had already committed. That turned every recovery into a support conversation. The checkpoint log should have been a user-facing artifact on day one, not an implementation detail.