Producing test data for microservices means keeping it consistent and referentially intact across services that each own their own database, where foreign key constraints can't span service boundaries. The dependable approach is to treat the distributed schema as one connected system: de-identify production with consistent, deterministic masking so shared identifiers line up across every service, pull a coherent subset that spans each service's database, and provision isolated environments per team so parallel work doesn't collide. Handle those three jobs together and a test dataset stays coherent, safe, and reproducible across the whole system.
Why test data breaks in distributed systems
The trouble starts with the database-per-service pattern behind most microservices architectures: every service owns its own store, and no other service reaches into it directly. That isolation lets teams deploy independently, but it also means a foreign key can't span a service boundary. The same logical entity — a customer, an order — lives as a real row in one service's database and is referenced only by ID in the others, so the database no longer enforces the relationships that hold the system together. They're now spread across many stores and, often, application code.
That is exactly where test data goes wrong. When you assemble data for a lower environment service by service, the references between those services drift out of sync: a customer that exists in the accounts service is missing from the orders service, or an order points at a payment record that was never copied. Tests that touch a single service pass, while integration tests that exercise the seams fail intermittently — flaky, non-reproducible failures that are hard to trace because the data, not the code, is the cause. Keeping referential integrity across services intact is the whole problem — the one a single database never forced you to think about.
Copying production into lower environments doesn't rescue you. At the scale distributed systems reach, cloning every service's full database is impractical, and it is unsafe: production carries real personal data that has no business in a test environment. Sound test data management for a distributed system has to reconstruct these relationships deliberately rather than inherit them from a copy. The recurring failure modes are worth naming:
- Orphaned references — a row points at an entity that was never pulled into another service, so a join or API call fails.
- Stale cross-service state — services refreshed at different times disagree about the same entity, and tests see a world that never existed.
- Schema drift — independently deployed services change their schemas on their own cadence, so data built against an older shape silently breaks suites.
Keeping referential integrity across service boundaries
The technique that holds a distributed test dataset together is consistency: the same input value has to transform to the same output value everywhere it appears. This is deterministic, consistent masking, and the emphasis is on consistent across every service's database rather than applied to each database in isolation. If the accounts service masks customer 4815 to 9027 while the orders service masks the same customer to something else, the reference is broken and every cross-service test built on it is meaningless. Applied deterministically, a given input always maps to the same output, so a masked identifier lines up across services exactly as the real one did.
Declared foreign keys are only part of the picture. In mature distributed systems, many of the relationships that matter never appear as database constraints at all — they live in application code, as a field one service populates with an ID it knows belongs to another. A tool that only reads declared constraints will miss them and produce data that looks internally valid per service but falls apart across the system. The masking layer has to understand these virtual relationships and treat them as first-class, so deterministic masking is applied to the linking fields wherever they occur, declared or not.
Tonic Structural is a clear example of how this works in practice. Structural applies consistent transformations from the real schema and preserves referential integrity across related tables, databases, and environments — so a customer ID masked in an orders service resolves to the same masked value in the payments service that references it. Configure the transformation once against the source, and the relationships survive into every environment the data lands in.
The Tonic Advantage: consistency that survives masking. The moment you mask data, naive per-column substitution breaks the links between tables and services. Tonic Structural's deterministic masking maps each input to a stable output, so a shared identifier stays identical everywhere it appears — across related tables, across separate service databases, and across every refreshed environment. The relationships that make a cross-service test meaningful are preserved by construction, not patched up afterward.
Building coherent test data subsets across services
You rarely want every service's full dataset in a test environment. What you want is a coherent slice — one customer together with their orders, payments, and shipments — pulled intact across every service database that holds a piece of them. The slice has to stay referentially complete as it crosses boundaries: pull the customer without their payment records, or the orders without the shipments they reference, and the subset arrives full of orphaned rows that fail on the first run.
Getting that slice right is a problem of database subsetting: traversing the dependency graph rather than filtering each database on its own. A subsetter that understands the distributed system walks from a starting entity through everything connected to it, handling the awkward cases distributed schemas are full of:
- Cross-database dependencies — follow a reference from one service's database into another and pull the related rows, so the slice stays whole across services.
- Application-level relationships — include the links that live in code rather than in declared constraints, the same virtual relationships that masking has to respect.
- Polymorphic keys — resolve fields that can point at more than one target table depending on a type value, a common pattern in event and audit tables.
- Cycles — detect and break circular dependencies between tables so traversal terminates instead of looping.
Tonic Structural's subsetter is built for exactly this: it traverses dependencies across databases, incorporates relationships defined in application code, and breaks cycles to produce a slice that is small enough to work with and still referentially intact across services. The payoff at scale is real — eBay scales a multi-petabyte data ecosystem down to manageable subsets, shortening development cycles and fueling automated testing, rather than wrestling with full-size copies for every environment.
Testing a service in isolation vs. integration testing
Two distinct needs sit underneath test data for microservices, and they call for different data. For integration testing, you need coordinated data that stays consistent across the services under test — the customer, the order, and the payment all present and correctly linked so the seams between services get exercised. That is the job the masking and subsetting approach handles: a coherent, referentially intact slice spanning every service the test touches.
Testing a single service in isolation is a different situation. Here you often need data for dependencies you don't own, or for services that don't exist yet — an upstream provider still on someone else's roadmap, or a third-party API you can't point at real data. Rather than reproduce those dependencies, you generate what the service under test needs from scratch and stand up mock services for it to call, so it has something realistic to exercise against without the rest of the system in place.
Tonic Fabricate is a useful option for this second job. Tonic Fabricate generates referentially intact data across databases, files, and APIs from a schema or a plain-language description, and it can create mock APIs from a spec — so a service in isolation gets coordinated test data across integrated systems it depends on and endpoints that behave like the real thing. Tonic Structural and Fabricate address different problems here — de-identifying and subsetting real production data for integration testing, versus generating synthetic test data when production isn't available for isolation testing — and on a distributed system you often use both, each for the job it fits.
Provisioning and refreshing test data for many teams
A distributed system is also a distributed org: many teams, many services, each deploying on its own schedule. Provisioning test data for that reality has to be self-service and repeatable, not a ticket that routes through a central data team every time an environment goes stale. The two things that break parallel work are shared environments where one team's data changes trip another team's tests, and stale data that silently drifts away from the current schema.
Isolated, ephemeral environments solve the first problem: when each team or branch gets its own short-lived dataset to spin up and tear down on demand, parallel work stops colliding in shared data. Automated refresh in the pipeline solves the second — regenerating test data on a schedule keeps environments current as services deploy independently, and detecting schema changes as they land means a service that alters its shape doesn't quietly break every downstream suite. A workable provisioning setup for distributed teams covers a few requirements:
- Self-service provisioning and refresh — teams get the data they need on demand, without a central bottleneck.
- Short-lived, disposable environments — isolated per team or branch, so no two teams share mutable state.
- Fresh, safe data inside pipelines — scheduled regeneration that keeps lower environments aligned with production's current shape.
- Schema-change detection — new columns and altered tables are caught, so stale data doesn't fail suites without explanation.
Tonic Structural is built around this workflow for testing and QA across many teams: it spins up isolated, ephemeral environments, uses its patented subsetter to give each team a targeted dataset that eliminates collisions, and supports scheduled generation with schema-change detection so refreshes stay accurate as services evolve.
The Tonic Advantage: keep every team unblocked. In a distributed org the constraint isn't producing one good dataset — it's producing many, kept current, without teams stepping on each other. Tonic Structural provisions isolated, ephemeral environments per team or branch and refreshes them on a schedule, with its patented subsetter carving out collision-free slices and schema-change detection catching drift before it breaks a suite. Parallel teams stay unblocked and their integration suites stay green.