The Feature Ends at a String Literal in Someone Else's Repository


Somebody asks how order creation works. You open the order service and start reading.

POST /orders
→ OrderController.create()
→ OrderService.submit()
→ orders table
→ publish("order.created")

And that's where the trail stops. Not because you've reached the end of the feature, but because you've reached the end of the repository.

order.created goes somewhere. Something consumes it, does work, probably writes to a table you've never opened. That part of the flow is in a repo you may not have cloned, owned by a team you may not have met.

Synchronous code tells you where it goes

This is the trade you make with events, and it's worth naming precisely because it's easy to forget you made it.

In a synchronous call chain, the code tells you the next hop. OrderService.submit() calls PaymentClient.charge(), and your editor will jump you there. The call stack is the architecture, written down and machine-checkable.

Events remove that. As one 2026 write-up puts it, event-driven architecture is coupling without a call stack. The coupling didn't disappear. Consumers still depend on event shape, ordering, and delivery semantics. It just moved into runtime behaviour, where you can only observe it in production logs and post-incident timelines.

The thing connecting producer to consumer is a string. "order.created" in one repository, "order.created" in another. No type checks that. No compiler notices when one side changes.

Which makes comprehension a search problem

The practical consequence for anyone new to a system, which included me not long ago, is that understanding an async feature means guessing well.

You grep for the event name across whichever repos you happen to have locally. If the consumer lives in a repo you haven't cloned, you find nothing and conclude nothing consumes it.

That conclusion is wrong in a specific and dangerous way. Finding nothing is not the same as there being nothing. It's the absence of evidence in the places you happened to look.

And event names are rarely clean to search for. They're built from constants, or namespaced by environment, or assembled at runtime from a prefix and a suffix. Grep for the literal and you miss all three.

There's a related problem the same analysis flags: multiple UserUpdated events may exist across different services, each meaning something slightly different. So a text match can also give you too much, and you're now reading three unrelated consumers trying to work out which one is yours.

Causality is implicit, not recorded

The other half of this is timing.

In a request-response system, failure is local. Something fails, a trace shows where, logs line up with causality.

With events, a failure may not surface until minutes or hours after the event was produced, in a consumer that has no obvious relationship to the thing you changed. Understanding why something happened means reconstructing an event flow rather than reading a stack.

Correlation IDs help enormously and every guide recommends them, correctly. The catch is that retrofitting them is miserable and they have to be carried from the first event onward. If your system already exists without them, that advice is a project rather than a fix.

Even with them, tracing tells you what happened at runtime. It doesn't tell you what could happen, which is the question you're actually asking when you're about to change the producer.

What I want instead of grep

The question I want answered is structural, not textual: which code publishes this event, and which code consumes it, across every repository, regardless of what the string is called locally.

That's a relationship question. PaymentService publishes payment.completed; InvoiceWorker consumes payment.completed; InvoiceWorker updates invoice.status. Three facts about the system, and none of them live in a single file.

This is the layer Vorr AI is built around. Rather than treating a codebase as a pile of files to search, it builds a relationship graph across repositories, with entities like functions, APIs, events, queues and database tables, and the edges between them. A feature is then a connected subgraph rather than a folder.

The practical difference for me is what an empty answer means. A grep that returns nothing means I searched wrong or looked in the wrong place. A graph query that returns nothing about consumers of an event means the indexed code contains no consumers, and I can check what's indexed. Those are very different levels of confidence, and the second one is the only one I'd act on.

The evidence matters as much as the answer. Repository, file, line. If a tool tells me InvoiceWorker consumes this event and can't show me where, I have to go and verify it anyway, at which point it saved me nothing.

Practical things, regardless of tooling

Keep event names in a shared constants module rather than as literals in both repos. It doesn't create a compile-time link across services, but it makes the search reliable and gives you one place to look.

Write down consumers next to the producer. A comment listing known consumers goes stale, but stale-and-close beats nothing, and it tells the next person which repos to clone.

Wire correlation IDs from day one if you're early enough. If you're not, at least add them at the boundaries you touch most.

Check whether your event names are actually greppable. If they're assembled at runtime, nobody after you will find them, including you in four months.

The thing I got wrong early on was assuming a feature has a location. Features in systems like this don't have a location. They have a shape, and it crosses repositories, which means the tools that work one repository at a time will always show you a piece of it and look like they showed you all of it.


Author note

Manjunaathaa, Associate DevOps Development Engineer at Frigga Cloud.

The bit that still catches me is how confident a partial answer feels. You read the producer, you understand it properly, and you genuinely do know how order creation works right up to the publish call.

Nothing signals that you're halfway. There's no error, no gap, just a line of code that ends and a system that carries on somewhere you can't see from here.

If your team has found a good way to keep producer and consumer relationships discoverable, I'd like to hear it. LinkedIn.

Post a Comment

Previous Post Next Post