How does Pact-JVM bind a pact's provider-state name to a @State method, and what breaks when two handlers seed the same record?
answer
- a bare string is the whole coupling
- the match is literal, not clever
- parameters beat inventing new names
- setup wants a matching teardown
- order shifts when interactions change
basics
~20 sPact-JVM matches the provider-state string recorded in the interaction against the value on a @State-annotated method by exact text, and that method seeds real provider data. Two handlers writing the same fixture rows collide, so results depend on interaction order.
solid answer
~50 sEach interaction in a V3 pact carries a `providerStates` array of names with optional parameters. Pact-JVM looks for a method in the verification test annotated `@State` whose value equals that name - a literal string match, so renaming a state on the consumer side makes the provider run fail looking for a handler that no longer exists. The handler can take a `Map<String, Object>` to receive the recorded parameters, and a second `@State` declared with the teardown action runs after the interaction. Its job is to put the provider's **real** data into that state - rows, caches, primed downstreams - never to mock the code under verification. The classic defect is shared fixtures: two handlers both writing grant reference `PB-4471` leave residue for each other, so the suite's result depends on the order the interactions happen to be replayed in.
code
java · 10 lines@State("a grant application exists")
void grantApplicationExists(Map<String, Object> params) {
String reference = (String) params.get("reference");
applications.save(new GrantApplication(reference, "PEAT_BOG_RESTORATION", 68341L));
}
@State(value = "a grant application exists", action = StateChangeAction.TEARDOWN)
void removeGrantApplication(Map<String, Object> params) {
applications.deleteByReference((String) params.get("reference"));
}go deeper
Recall that the provider needs the right data in place before a recorded request can be replayed, and that a named setup routine on the provider side is what puts it there. You are not expected to design that setup yet.
Explain the binding precisely: the state name in the interaction is matched literally against the annotation value, parameters travel with it, and a teardown counterpart can be declared for the same name.
Show that you have debugged this for real. Be ready to diagnose a suite whose result changes with interaction order and to name fixture overlap as the cause rather than calling the suite flaky.
Own the state vocabulary across teams: how many names a provider can sustain, when to parameterise instead of naming, and how to stop a cross-repository string coupling turning into a standing coordination tax.
## From a name in JSON to a method in the provider's test A V3 pact records provider states on each interaction as an array of objects carrying a `name` and an optional `params` map. That name is free text chosen by whoever wrote the consumer test - "a grant application exists", "the applicant has no submitted claims" - and it is the entire coupling between two repositories that never compile together. On the provider side, Pact-JVM looks through the verification test class for a method annotated `@State` whose value equals that string. The match is **literal**: no pattern, no normalisation, no fuzzy matching. Two consequences follow immediately, and both turn up in real teams: - Rename a state on the consumer side and the provider's next verification run fails looking for a handler that no longer exists. The failure is loud, which is good, but it is cross-repository work that no compiler warned anybody about. - Because names are free text, a provider accumulates near-duplicates - "grant exists", "a grant exists", "an existing grant" - each with its own handler seeding almost the same data. The practical discipline is to keep the vocabulary of state names small and deliberately boring, and to push variation into parameters rather than into new names. ## Parameters: one handler covering many interactions A handler may declare a `Map<String, Object>` parameter to receive the `params` recorded with the state. One handler for "a grant application exists" that reads a `reference` parameter can then serve twenty interactions each needing a different application, instead of twenty near-identical handlers. It is also the mechanism that lets a consumer say *which* record it means without hard-coding a database identifier into the pact and freezing the provider's schema by accident. ## Setup, teardown, and what a handler may touch A second `@State` for the same name, declared with the teardown action, runs after the interaction has been replayed. That pairing is what makes a suite order-independent. | Belongs in a state handler | Does not belong in a state handler | | --- | --- | | Inserting or updating the provider's own records | Assertions about the response | | Priming a cache or seeding a queue the provider reads | Mocking the provider's own controller or service | | Configuring a stand-in for something the provider calls out to | Sleeps, retries or waiting for something to settle | | Removing exactly what this handler created, on teardown | Wholesale truncation of the database between interactions | The line that matters most: a handler prepares **data the provider will read**, never **behaviour the provider under verification would otherwise perform**. Replace the controller or the service with a mock and the run proves that the mock honours the pact, which is worth precisely nothing. ## The shared-fixture failure mode Take a peat-bog restoration grant portal whose applications endpoint returns a 17-field body. One handler, for "a grant application exists", inserts reference `PB-4471` with an awarded amount of 68341. A second handler, added months later for "a grant application has been withdrawn", also inserts `PB-4471`, because that reference was already sitting in the test fixtures and it was convenient. Nothing fails on the day it is written. What happens instead: 1. Both interactions pass locally, because they happen to run in an order where the second handler's write lands after the first interaction has already read its row. 2. A new consumer publishes a pact, the provider fetches one more interaction, and the expansion order changes. 3. Now the withdrawn row exists when the first interaction is replayed, `status` comes back as `WITHDRAWN` instead of `AWARDED`, and an interaction nobody touched turns red. 4. The team, in the middle of a platform migration running in parallel, spends a day hunting a regression in code that did not change. The diagnostic tell is twofold: the failure moves when the interaction set changes, and re-running the single failing case in isolation passes. On a column with a unique constraint the symptom looks different - the second insert throws inside the handler, so the interaction fails during setup rather than during comparison - but the cause is identical. Note also what does *not* happen: Pact never inspects your fixtures, so nothing warns you at load time that two handlers overlap. ## Keeping handlers order-independent 1. **Give each state disjoint identifiers.** One state, one fixture namespace; `PB-4471` belongs to exactly one handler and no other handler may touch it. 2. **Make writes idempotent.** Create-if-absent rather than blind insert, so applying the same state twice is harmless. 3. **Pair every setup with a teardown** that removes precisely what the setup created - not a blanket wipe, which is slow and merely hides the coupling in the other direction. 4. **Never rely on replay order.** It is not part of the contract and it changes whenever any consumer adds, removes or reorders an interaction. 5. **Prefer parameters to new names**, so the number of handlers grows far more slowly than the number of interactions. Handlers written this way are the difference between a provider verification suite that survives four consumers and one that is quietly switched off after its third mystery failure.
- A consumer renames a provider state. What happens on the provider's next verification run?The run fails for that interaction because no `@State` method matches the new name. Nothing catches it earlier: the state name is an untyped string crossing a repository boundary, so no compiler, no type check and no review in either repo sees the break. Keeping the vocabulary of names small and stable, and expressing variation through state parameters instead, is what keeps rename cost low.
- How do you stop forty interactions needing forty different provider states?Parameterise. One state name such as 'a grant application exists', with the identifier passed as a state parameter, covers every interaction that needs some application to exist. The handler reads the parameter map and seeds that specific record. Fewer names means fewer cross-repository string couplings, fewer handlers, and dramatically fewer fixtures with the opportunity to collide.
- How do you make state handlers safe against the order interactions are replayed in?Give each handler its own disjoint identifiers so no two handlers write the same rows, make the writes idempotent so re-applying a state is harmless, and pair each setup with a teardown that removes exactly what it created rather than truncating everything. Replay order is not part of the contract and shifts whenever a consumer adds or removes an interaction.
saying these in an interview costs you the question
- Thinks the state name is matched by regex or loosely normalised
- Puts response assertions in the state handler instead of data setup
- Mocks the provider's own service or controller inside a state handler
- Assumes interactions always replay in the order stored in the file
- Writes setup handlers with no teardown and shared fixture rows