← All experience

The numbers were wrong for two days before anyone noticed

MeeshoAssociate Product Manager II, Data Platforms Apr — Oct 2025Internal data platform

Building data quality detection at Meesho.

The short version

Meesho's analysts, data scientists and product managers all build on a shared set of core data tables. When one of those tables quietly broke, nobody found out until someone hit the bad data in their own work and complained. That took about two days.

I scoped a three-phase program to fix this and shipped the first phase: automated quality checks that watch the core tables and alert when something looks wrong. It launched on 5 tables and grew to 16. Issues started surfacing and getting fixed the same day instead of two days later.

The interesting decisions were not the checks themselves. They were about trust, scope, and cost.

What was actually broken

Some context, because this is invisible from outside a large company.

Meesho is one of India's largest e-commerce platforms. Almost every decision inside it runs on data: what to stock, what to price, which sellers to promote, whether a feature worked. That data lives in tables, and a small number of those tables are the ones everyone else builds on. Internally they were called platinum tables. If a platinum table is wrong, everything downstream is wrong too, and confidently so.

The failure mode is that data does not break loudly. A column starts arriving empty. A join silently drops a category. Row counts fall by a third because an upstream pipeline half-failed. Nothing crashes. The dashboards still render. The numbers are just wrong.

So the way anyone found out was by collision. An analyst would be halfway through a piece of work, notice a number that made no sense, dig into it, discover the table was broken, and report it. The issue would land on day one, get reported on day two, and get fixed on day three.

That is two days of people making decisions on bad numbers, plus the wasted work of whoever eventually tripped over it.

BeforeTwo days of decisions on bad numbers
Two days of decisions on bad numbers
Day 1A table breaks. Nothing crashes.
Day 2Someone hits the bad data and reports it
Day 3Fixed
After
Day 1A check fires with the failing rows attached, and the issue is handled the same day

The three phases

I scoped the program around where in the lifecycle you intervene, not around building more of the same thing.

Phase 1Shipped

Detection

Catch the problem when it happens rather than when someone bumps into it.

Phase 2Planned

Resolution

When a problem is caught, walk backwards through the data's lineage to find the table where the corruption actually started. Where it surfaces and where it originates are usually different places, and the gap between them is most of the debugging time.

Phase 3Planned

Reduction

Stop it happening. Whenever anyone edits a table anywhere in a pipeline, diff the output before and after and flag it if the numbers move unexpectedly. Effectively continuous integration, applied to data instead of code.

I shipped phase 1. I planned phases 2 and 3 but left the company before either was built. Phase 3 existed in a crude manual form already: when a code change went in, we would generate a report showing how that table's averages and key metrics moved before and after. Doing it by hand is what convinced me it was worth automating.

I had about six months. Scoping three phases knowing I would probably only finish one was deliberate. The sequence was the argument I needed to make internally, even if I was only going to build the front of it.

What I built

Phase 1 is a data quality checks system, and it lives as a tab inside Meesho's existing Metric Store, the internal tool where people already go to look at table definitions, lineage and queries.

The Data Quality Checks tab sits next to the template, lineage and query tabs people already used. Screens on this page are from the phase 1 design prototype.

That placement was the first real decision. An internal product has no marketing, no growth loop and no way to make anyone use it. The only lever is friction. Putting the checks inside a surface people already open every day meant nobody had to adopt a new tool, learn a new URL, or remember it existed.

Seven check types, plus an escape hatch. Most data breaks in a small number of predictable ways, so I made templates for them: null checks, required fields, row counts, value distribution, allowed-value lists, cross-field rules, and aggregate ranges. Someone can add a check by filling in a form in about a minute. But templates never cover everything, so there is also a custom option where you write raw SQL or Python. Cover the common cases with configuration, and do not block the person whose problem you did not anticipate.

Templates for the common failures, and custom SQL or Python for the rest. Cross-field and custom checks were in the prototype but cut from the first shipped version.

Checks apply to a table or to a template. A template-level check propagates to every table built from that template. This is the mechanism that let the system go from 5 tables to 16 without 16 separate efforts.

Checks run on a schedule or on an event. Hourly through monthly for time-based, or triggered by a data insert, an update, a schema change, or a pipeline finishing. Not all corruption is time-correlated, so waiting for the next scheduled run is sometimes two hours too late.

Time-based checks run hourly to monthly. Event-based checks run when data lands, changes shape, or a pipeline finishes.

The two decisions I would defend hardest

You can mark an alert as a false positive.

Most data quality tools only fire alerts. I built an explicit path for a user to say the alert was wrong.

The real failure mode of any alerting system is not missing errors. It is losing trust. Once people believe the alerts are noise, they mute them, and then the system is worse than useless because it costs money to run and produces nothing. Giving people a way to disagree with the system keeps them engaged with it instead of routing around it.

Every alert carries its own Mark False Positive action.

An alert tells you what broke, not just that something broke.

When a check fails, the alert carries the expected threshold against the actual value, the total record count against the failed record count, and sample failing rows with their actual values and the reason each one failed.

A failed check shows the threshold against the actual value, how many rows failed, and samples of the rows themselves.

The difference between "null check failed on customer_id" and "1,230 of 10,000 rows are null, here are three of them" is the difference between an alert someone investigates and an alert someone dismisses. That detail is the whole mechanism behind the time saving. It is not that we found problems faster. It is that the alert contained enough to act on immediately.

The trade-off I had to make

Phase 2, the root-cause tracing, only works properly if the instrumentation exists across every layer of the data lineage. If you can only see part of the chain, you cannot reliably walk back to the origin.

Full coverage was expensive, and the engineering manager pushed back on the running cost. This was a product with no revenue attached to it, and someone had to justify why it should consume compute indefinitely.

We scoped down to a few tables and planned to expand later. I think that was right. A tracer covering part of the lineage still catches most issues, and waiting for full coverage would have shipped nothing at all. The same constraint applied to phase 1: cross-table checks and custom checks were both cut from the first version because of engineering bandwidth, and it launched on 5 tables rather than everything.

Results

5 → 16 tables Launched on 5 core tables, grew to 16 before I left
Day 3 → same day Issues moved from surfacing on day three to being handled the same day
Weekly on-call A rotating on-call from the data platform team owned alert response, so every alert had someone accountable for it

Some checks shipped on by default; table owners added the rest.

On the time saving: the old three-day pattern was well understood, and I tracked the new one roughly as issues came in rather than through formal instrumentation. I would not present it as a measured metric. The mechanism is the more useful claim: detection removed a discovery lag that depended entirely on someone happening to hit the bad data in their own work.

What I did not solve

If one upstream table breaks, every check downstream of it fires at once, and the on-call gets a wall of alerts that are all the same incident. Phase 2 was the answer to that and I did not get to build it.

There was also no learning loop. Marking an alert as a false positive recorded the fact, but nothing fed that back into tuning the check. That is the first thing I would build next.