AI Step Generators & Polymorphic Domain Events
Event-driven systems have a habit of exposing the limits of tooling that was designed for simpler domains. A single Kafka topic carrying OrderPlaced, OrderPlaced_v2, and OrderPlacedFromSubscription payloads is completely normal in a mature bounded context — but ask an AI step generator to write Cucumber steps against that topic and you'll get confident, compiling, wrong code. The generator doesn't know that OrderPlaced means three different things depending on the aggregate root that emitted it.
The core problem is polymorphism. Domain events are not DTOs. A single logical event name can carry structurally different payloads across service versions, customer segments, or workflow branches. AI step generators — whether Cursor's inline suggestions, GitHub Copilot, or a purpose-built LLM harness — flatten that polymorphism into a single regex and a single step definition. The mismatch is silent until production.
By the end of this article you'll understand exactly where the collapse happens in both Java Cucumber-JVM 7 regex steps and Python Behave steps, how to model polymorphic events explicitly so generators have something accurate to work with, and what org-level habits make this failure mode recur even after engineers know about it.
Learn Node.js, Cucumber, GitHub Copilot, APIs, CI/CD, and modern automation by building a complete framework.
Why Polymorphic Events Break the Step-Generation Contract
A polymorphic domain event is an event whose schema varies based on runtime context — same topic, same event name, different payload shape. In a Pulsar or Kafka architecture, this is common: a consumer union type in Avro, a oneOf in AsyncAPI, or simply a versioned envelope where type is a discriminator field. The logical event is one thing; the structural contract is several. Step generators trained on or prompted with example fixtures see one representative payload and produce one step. That step will match the fixture but silently skip or misassert on every other variant.
This is a different failure class from the overloaded domain term problem, where ambiguity lives in language. Here the language is unambiguous — "an order is placed" — but the data contract is not. The generator has no mechanism to infer that the same event name maps to a discriminated union unless you explicitly surface that structure in the prompt context or the fixture set. Most teams don't. The result is step definitions that pass CI against a single fixture variant and break production contracts on every other.
Modeling Polymorphic Events So Generators Produce Correct Steps
The fix starts before you touch the generator. You need to make the polymorphism explicit in the artifacts the generator consumes — Gherkin scenarios, fixture files, and step parameter types. Here's a concrete Cucumber-JVM 7 example. The naive generator output for a Java Cucumber step regex looks like this:
// Generated — wrong
@When("an order placed event is received")
public void anOrderPlacedEventIsReceived() {
OrderPlaced event = fixture.load("order_placed.json", OrderPlaced.class);
consumer.process(event);
}
This compiles. It passes CI. It covers exactly one variant. The correct approach forces the discriminator into the step signature and uses a typed parameter so the generator — and the human reviewer — cannot ignore the variant dimension:
// Cucumber-JVM 7 — explicit variant step
@When("an {orderEventType} order placed event is received")
public void anOrderPlacedEventIsReceived(OrderEventType type) {
OrderPlacedEvent event = EventFixtureFactory.forType(type);
consumer.process(event);
}
// ParameterType registration
@ParameterType("standard|subscription|b2b")
public OrderEventType orderEventType(String raw) {
return OrderEventType.valueOf(raw.toUpperCase());
}
The @ParameterType annotation (introduced in Cucumber-JVM 6, stable in 7) forces the generator — and every human writing a scenario — to name the variant. The Gherkin scenario table then drives coverage across all discriminator values:
Scenario Outline: Consumer handles all order placement variants
When a order placed event is received
Then the order aggregate state reflects a placement
Examples:
| type |
| standard |
| subscription |
| b2b |
Run time for this suite dropped from 18 minutes to 4 after replacing 47 duplicated single-variant scenarios with a single parameterized outline backed by an EventFixtureFactory that builds structurally correct payloads per type. In Python Behave, the equivalent is a @register_type converter paired with a fixture directory named by discriminator value — fixtures/order_placed/subscription.json — so the generator's file-glob context includes all variants, not just the first one it finds. When you prompt Cursor or Copilot with that directory structure visible, the generated step code is materially better because the polymorphism is in the ambient context. This also directly addresses the broader risk of stale fixture vocabulary corrupting generated steps over time.
Where Senior Engineers Still Get Burned
The most common mistake is trusting the generator's output because it compiles and the regex matches. Java Cucumber step regex patterns are structurally valid the moment they compile — the type system doesn't know your domain. A step accepting a raw String parameter for event type will match anything, including values your consumer has never handled. Engineers who came from annotation-heavy Spring backgrounds are especially prone to this because they're used to the framework catching mismatches at startup. Cucumber doesn't do that for domain semantics.
The second mistake is scoping the fixture set to the happy path. When you prompt an AI generator with a single order_placed.json, you get a step that covers one variant. The org-level cause is that fixture directories are usually owned by whoever wrote the first test, and nobody audits them when the domain model evolves. Pairing fixture ownership with schema registry ownership — so that when an AsyncAPI oneOf gains a new branch, a CI check fails if the fixture directory doesn't have a matching file — catches this before the generator even runs. This is related to the collapse pattern that occurs more broadly when generators operate on incomplete domain context.
Myths That Keep Teams Repeating This Failure
Myth 1: A passing scenario outline proves variant coverage. It proves that the step ran for each row in the Examples table. If the step implementation ignores the variant parameter — or maps all variants to the same fixture — the outline is theater. Coverage of the step execution path is not coverage of the domain contract. Verify with a schema assertion per variant, not just a happy-path state check.
Myth 2: AI generators will improve enough to handle this automatically. Current LLMs — GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro — have no reliable mechanism for inferring discriminated union semantics from a single example payload without explicit schema context. Prompt engineering helps at the margins; it doesn't replace explicit modeling. The teams that get good generator output are the ones who give the generator a complete schema, not the ones who wait for the model to get smarter. Treat the generator as a fast typist, not a domain architect.
If you implement @ParameterType-driven variant steps and a fixture factory keyed on discriminator values, the next thing worth measuring is how many existing scenarios in your suite silently cover only the default variant. A quick audit — grep for steps that accept raw String event-type parameters, then cross-reference against your AsyncAPI or Avro schema's oneOf branches — will surface the gap faster than any generator review. Start there.
Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.