A bank cannot lose your rent payment, and it cannot pay your house deposit twice. That constraint is what makes event-driven architecture in a regulated industry a different problem from event-driven architecture in analytics or IoT, where dropping one event in a hundred thousand is an acceptable rounding error. Chris Tacey-Green's thesis is that the benefits of eventing — decoupling, an immutable activity log, fan-out, fault tolerance, plug-and-play extensibility — are real and worth having in banking, but that you only get them safely if you build the protective patterns into your platform up front rather than discovering the need for them at 2 a.m.
Tacey-Green is Head of Engineering at Investec, a bank he describes as unusually modern for the sector. The 51-minute talk was recorded at InfoQ Dev Summit Munich and published on April 20, 2026. His examples are drawn from systems Investec runs in production on Azure. These notes report what he presented; where I add background for readers new to the topic, it is labeled as such.
What You Will Learn
- The precise difference between a command and an event, and why mixing them quietly cancels the benefits of an event-driven system.
- Why event sourcing and event-driven architecture are separate decisions, and why conflating them is expensive.
- Five concrete benefits of eventing illustrated with real banking use cases: decoupling, immutable activity logs, fan-out, layered fault tolerance, and plug-and-play capability delivery.
- How the outbox pattern stops you losing events and how the inbox pattern stops you processing them twice — and why you need both.
- Why an event contract is harder to change than a REST API, and how to version one so that replay from the beginning of time stays safe.
- Why separating domain events from integration events protects your ability to refactor your own domain.
- Two workable approaches to event ordering, and the scalability price each one charges.
- The organizational cost — Tacey-Green's team measured roughly six months for a new joiner to reach full delivery pace in one event-sourced space.
Foundations: Event, Event-Driven, Cloud-Native, Banking
Tacey-Green opens by decomposing the title, because each word carries a definition that the rest of the talk depends on.
An event is a change in state somewhere in the system. It might be caused by a user action, by an asynchronous background task, or by an external system. Events differ in how much data they carry, and Tacey-Green borrows a dietary vocabulary that circulates in the community — he notes there is a well-known paper on putting your events on a diet. A fat event carries data — in a later Q&A answer he sharpens this to carrying the entire entity state around the system. A thin event is simply a notification that something happened. He recommends lean events: everything that genuinely pertains to the event, and nothing else.
The reason to prefer lean over thin came up in the Q&A. If you publish pure notifications, every consumer has to call back into your API to find out what actually changed, and you have re-coupled the systems you were trying to separate. A lean event, designed carefully so it carries the data that legitimately belongs to that event type, makes that callback unlikely. Fat events avoid the callback too, but at the cost of shipping your whole entity model — and therefore your internal structure — to everyone downstream.
Commands Versus Events
This distinction is the one Tacey-Green says he ends up arguing about "time after time," and his warning is blunt: if you build an event-driven system and then start pumping commands around it, you do not get the benefits you wanted, and you will hurt yourself later.
| Command | Event | |
|---|---|---|
| Intent | "I want something to happen" | "Something happened" |
| Target | A specific, named recipient | Anyone; possibly nobody |
| Expectation | A result, even if delivered asynchronously | None |
| Coupling | The sender knows the receiver exists | The publisher does not |
His image for an event is shouting into the world. You are not expecting anything to happen, and you are not necessarily expecting anyone to be listening. Nobody subscribing to your event is a perfectly valid outcome. A command, by contrast, is an explicit request aimed at a known party, and you are waiting for a result even when the delivery mechanism is asynchronous.
An event-driven architecture is then simply multiple systems reacting to events, consisting of producers that publish and consumers that receive.
Event Sourcing Is a Separate Decision
Tacey-Green asks the audience to spread this to their teams: event sourcing and event-driven architecture are not the same thing, and you do not need the former to do the latter.
Event sourcing is about how the state of your application is represented — as an
immutable sequence of events rather than as current values. His example is a
shopping cart holding four hats. Without event sourcing, the database contains
one record saying hats × 4. With event sourcing, the database contains four
records, each representing the addition of a hat, and to know the current state
you must replay them in order.
He is candid that this is a complicated pattern to apply, that he has seen people really struggle to understand it, and that it takes time to learn. The two ideas travel together because once you have event sourcing, subscribing to those events is a small additional step — but "understand there are dragons here."
Cloud-native, in his usage, means designing, constructing, and operating workloads in the cloud using modern engineering practice: highly scalable, typically microservice-based (he explicitly allows that modular monoliths are a good pattern too), deployed with modern DevOps and CI/CD.
Banking he defines with some affection and some sarcasm: large, slow, highly regulated organizations that keep your cash safe so you do not have to put it under your own mattress, and that tend to be terrified of the modern practices just described. Many still use fax machines as an integration mechanism. Investec, he says, is not one of them.
Why Eventing: Five Benefits From Real Banking Use Cases
Tacey-Green deliberately picks benefits that map to problems Investec had to solve rather than reciting a generic list.
Decoupling
The use case is transaction monitoring: watching everything that happens on a client's account and flagging anything abnormal, such as spending appearing suddenly in a country the client has never visited. Transaction monitoring needs a great deal of data from the payments system.
Without events there are two options, and both create coupling. Either payments pushes data to an API on transaction monitoring, or transaction monitoring pulls from an API on payments. Either way two systems are now bound together that should be independent, and — importantly — that have very different reliability expectations. Payments is crucial, heavily regulated (he points to PSD2 for anyone who wants to read the regulation), and must be built with reliability at its core. Transaction monitoring happens behind the scenes; fraud checks sit in the payment flow, but active transaction monitoring does not have to.
With events, payments has no idea transaction monitoring exists. It publishes
PaymentInitiated — carrying the user's location, the channel, the creditor and
debtor — and PaymentProcessed, carrying the gateway used. Transaction
monitoring subscribes to the events it needs and can later subscribe to more
without payments changing at all. Crucially, transaction monitoring can go down
without taking payments with it.
An Immutable Activity Log
Before the move to events, Investec found it hard to know where a payment was across the many flow points inside a bank: fraud checks, sanctions screening, gateway selection, and the varied responses gateways return once a payment is sent.
The event-driven version gave them an immutable activity log as a by-product. The key point Tacey-Green stresses is that this is not an audit log bolted on the side, and not application logs shipped to an aggregator and then correlated after the fact. The events are how the system runs, so the team trusts them. With business-oriented event names — which he flags as an important discipline in domain design — the business can see that a payment was initiated, that a fraud check completed, or that a fraud check is still outstanding because it fell into a manual operational process.
(Supplementary note, not from the talk: an activity log derived from the system's own execution path avoids the classic drift where audit logging is updated less often than the logic it describes.)
Fan-Out
He notes in passing that fan-in is the mirror-image pattern; this example is fan-out. After a payment completes, two things must happen. Payment limits must be updated — a client might have a £10,000 daily ceiling, and the system needs to know where they stand against it. Client communications must go out: a push notification, an SMS, an email, "a pigeon."
You can solve this without events, but you end up wrapping the two together and
then answering awkward questions. If updating payment limits fails, do you hold
the communication? Do you send it anyway and fix limits afterwards? A single
PaymentProcessed event with two independent subscribers removes the question.
Client communications does not care about the payment limits service and should
not have to, and each subscriber handles its own retries and fallouts. In
reality, he notes, there are more than two subscribers.
Layered Fault Tolerance
In a regulated industry you must tolerate faults, because the work is not optional. Tacey-Green's real example is an externally supplied fraud engine with reliability problems Investec cannot fix, because they did not write it. Events gave them three places to handle failure, and he stresses that you customize these levels however you like based on the domain and the use case you are solving for.
Level one: transient retries. In-process retries, no different from what most engineers already write — he cites Polly in .NET, declaring that you are happy to retry five times with a bit of jitter and a couple of seconds' wait, hoping a network blip clears. The extra benefit in an event-driven system is that the whole flow is already asynchronous and eventually consistent, so you can afford to stretch those retries out further than you otherwise would.
Level two: back off to the eventing technology. If transient retries keep failing, hand the problem back to the broker. He is emphatic that the technology choice does not matter here — Kinesis, Azure Event Hubs, a managed Kafka instance — because they all support this configuration. You back off further, for as long as the organization is comfortable with.
Level three: dead-letter and wake a human. Dead lettering exists primarily to deal with poisonous events: a message that breaks your eventing contract or carries data that simply cannot be processed. Without an escape route it retries forever and you end up, in his words, screwing around in databases to fix it. Once dead-lettered, you alert a human — potentially at 2 a.m. — who inspects the event and decides whether to replay it.
Plug and Play
The final benefit is what maturity buys you. Investec wanted to build a new rewards capability. Because platforms such as payments, accounts, and client already publish well-defined, domain-designed events, rewards might be built without bothering any of those teams — his wording is deliberately conditional, because it holds only if the events are good. Where they are, the capability needs permissions to the relevant event streams, and from those streams it learns when a client is onboarded, when an account is created, and when payments are processed.
What Hurts, and What Helps
It Is Hard for People
The first pain point is not technical. Event-driven architecture is hard for people who have not worked with it, and Tacey-Green says Investec sees this in architects, engineers, and especially new joiners. In one space that combined event sourcing and event-driven architecture, it took roughly six months for a new joiner to reach the delivery pace of the engineers already on the team. He offers this as a real organizational cost that gets ignored while everyone debates the technical trade-offs.
The failure mode is subtle: teams stepping into this world forget they are in a different paradigm. They solve problems that no longer exist and miss the ones that now do — eventual consistency, fault tolerance, ordering.
Three things helped. Developer platform artifacts: service templates that give an engineer a well-shaped event-driven microservice to start from, and application modules that solve the recurring problems once instead of once per engineer. Do this early. Training that is not documentation. Investec paired an enablement team with a delivery team that had never built an event-driven system, and blocked out an entire week. They taught the concepts, then designed and built a small real system in the delivery team's own space — by day five it was working and close to production. He acknowledges this does not scale, but that team now builds event-driven systems confidently. Aligned standards and principles across the estate, agreed early and written down: event contract conventions, the permissions model for event streams, and ideally the technology behind those streams. Without that, consuming another team's events is a new learning exercise every time and you never find pace.
A warning he makes explicit: giving teams a platform that lets them push event-driven systems into production without training them is dangerous, because when the system falls over in the middle of the night they will not understand what the platform's magic is doing.
Duplicating and Losing Events
This is the pain that is specific to regulated domains. These are the two failure modes from the opening, and they are the speaker's own examples: lose an event and the landlord never receives the rent; duplicate one and the house deposit goes out twice. In analytics or IoT you can afford to drop one event in a hundred thousand; a bank cannot. Tacey-Green insists this requires design and build up front — leave it until later and you will hurt yourself.
The outbox pattern protects the publisher against losing events. In his
onboarding example, when the system writes the new client to the clients table it
draws a transaction around that write and an insert into an outbox table
containing the ClientOnboarded event with a unique ID. State change and event
are now committed in the same transactional boundary. Without it, you can update
state successfully and then fail while publishing, and the event-driven benefits
you wanted evaporate. A separate dispatcher — polling the outbox table is
perfectly fine — reads those rows and publishes them to Kafka, Kinesis, Event
Hubs, or whatever you chose.
What the outbox explicitly does not solve is duplication. The dispatcher can publish the same row twice, and the broker itself may only offer at-least-once delivery.
The inbox pattern handles that on the consumer side. On receiving
ClientOnboarded, the consumer does not immediately run business logic — where
it might fail for genuine validation reasons or hit a transient problem. It first
writes the event ID and data to an inbox table, and only then executes the
business logic. If the same event arrives again, the consumer checks the ID, sees
it has been handled, and skips it.
An audience member from a similar industry asked about the case where inbox lookups are too expensive but duplication is unaffordable. Tacey-Green's answer: if you truly cannot run an inbox, you are relying on idempotency in your downstreams — every API call carrying an idempotency key header, with every downstream genuinely implementing it. With solid downstream idempotency you can probably get away without an inbox. With neither, you are simply carrying risk. "There's no magic bullet." His preference is to do both.
Breaking Event Contracts
Events decouple systems, but Tacey-Green is careful to say you are still coupled — by the events themselves. An event is a contract you have published to the world, and you cannot take it back.
The reason is replay. Events land on an immutable stream that reaches back to the beginning of time, and any consumer has the right to replay from there. Once a property exists on a published event, removing it is a breaking change, and consumers failing because of it is a painful remediation. He notes that some companies respond by editing events in the datastore or the stream — which means the stream is no longer immutable — and asks people not to do that.
What helps is treating the event like an API contract, because engineers already
have good instincts about not breaking those. Design events carefully; assume
every property is now public; avoid breaking changes; and when you cannot,
version them the way you would version a REST API. Some event standards, he
notes, include a data version property in the metadata for exactly this. A consumer then
branches on that property — effectively an if-else — handling v1 one way and v2
another, whether the change is a removed property or a changed data type. He adds
that if the whole structure changes, that is arguably a different event rather
than a new version. With versioning in place, replay from the beginning of a
stream that contains v1, v1, v1, v1, v2 remains safe.
The second thing that helps is separating domain events from integration events, covered in the architecture section below.
Event Ordering
Unless you explicitly configure it, cloud-native eventing technology does not care about the order of your events. These systems are built for scale — the vendors will happily claim a million events a second — and that throughput is possible partly because ordering is not guaranteed. Your retries do not respect ordering either: an event that backed off and retried is now being processed independently of the events around it.
The stakes in banking are obvious. Allowing a client to make two $1 million payments because the balance had not been updated yet is, in Tacey-Green's phrase, "slightly career-limiting." Ordering is not a no-go, but introducing it carries risk and cost. He offers two approaches.
Explicit ordering via a version stamp. Stamp each event for an aggregate with an incrementing version: 1, 2, 3, 4. In the inbox — in the shared framework every team is already using — add an ordering check. For a given aggregate, if the consumer receives version 2 but has not seen version 1, it does not process it; it backs off and returns the event to the stream. Eventually version 1 arrives and is processed, the retry of version 2 succeeds, and consistency is restored. The cost is scalability: you have effectively built a queue inside your event-driven architecture without using queueing technology. Investec has real implementations of this where the ordering is genuinely needed, and it works — you just have to accept the throughput impact.
An audience member pushed on how you assign that version, contrasting a database-derived counter with a timestamp. Tacey-Green acknowledged both are used in practice and that people argue about it online. Investec went with the incrementing counter, which brings a competing-writes problem: two simultaneous reads can both compute the same next version. The mitigation is a unique index on the aggregate ID plus version, so two events cannot both claim version 2. That constraint is another reason the approach scales worse.
Implicit ordering via domain validation. Instead of stamping events, let the domain decide which events it can currently process. His example: you cannot pay a beneficiary until you have seen that the beneficiary was created, because you do not have their details. Ordering emerges from the business rule rather than from metadata. Investec has a platform doing exactly this that has never needed version stamps, and it works fine.
Architecture And Data Flow
The closing diagram ties everything together in a payments-and-communications flow, and it is where the domain/integration split becomes concrete.
A domain event lives inside a bounded context. Its name can be as verbose and
specific as the owning team likes, because nobody outside sees it —
SwiftFPSPaymentProcessed is Tacey-Green's example. An integration event is
what crosses domain boundaries, and it is modeled deliberately and separately.
The point of the split is to stop domain concepts bleeding out. Once you
accidentally publish an internal concept and someone consumes it, you have
contractually committed to it and can no longer refactor your own domain freely.
He notes that some people will see a similarity to an ACL here: you are
protecting your boundary.
The integration event publisher — built into the service template so nobody
has to remember to do it — performs three jobs. It filters, because not every
domain event should become an integration event. It aggregates, because
several domain events may fan in to a single integration event. And it
transforms, stripping the properties that should not leave the domain. What
emerges is a plainly named PaymentProcessed.
flowchart TB
subgraph Payments["Payments domain"]
API["Payment API"] --> DB[("Payments DB
+ outbox
same transaction")]
DB --> Disp["Outbox dispatcher"]
Disp --> DStream(["Domain event stream"])
DStream --> DEH["Domain event handler
inbox: dedupe
SwiftFPSPaymentProcessed"]
DEH --> IEP["Integration event publisher
filter / aggregate / transform"]
end
IEP -->|"PaymentProcessed"| Bus(["Integration event stream"])
subgraph Comms["Communications domain"]
IEH["Integration event handler
inbox: dedupe"] --> Trans["filter / aggregate / transform
to domain events"]
Trans --> Work["Send SMS
SmsDelivered domain event"]
Work --> IEP2["Integration event publisher
filter / aggregate / transform"]
end
Bus --> IEH
IEP2 -->|"CommunicationSent"| BusReading the flow: a caller hits the payments API; the payment is written to the
payments database and the domain event is written to the outbox in the same
transaction, so the event cannot be lost. The dispatcher then publishes it onto
the eventing technology, and that redelivery-capable hop is exactly why the
internal domain event handler sits behind an inbox: the same domain event can
arrive more than once. The integration event publisher filters, aggregates, and
transforms the verbose domain event into PaymentProcessed. The communications
domain has its own integration event handler behind its own inbox — which is what
stops a client receiving the same SMS several times — and its own filtering and
transformation into its internal domain events. Tacey-Green elided the repeated
machinery on his slide for brevity, so the communications side of the diagram
above is drawn less completely than the payments side. On the way back out, SmsDelivered becomes
CommunicationSent. Tacey-Green's own framing is that nobody should expect to
build this from one slide, but that every box is there deliberately: this is what
event-driven architecture in a highly regulated industry looks like with the
protections in place. Investec built these protections into its developer
platform, which is why teams have not had to rediscover them during an incident.
An audience member asked what happens when there are not two domains but many, and 300 integration event types. Tacey-Green described a third level: platform-level events, which Investec calls public events — public to the organization rather than the internet. The same filter/aggregate/transform protection applies at that boundary. Noise decreases as you climb: domain events are the noisiest, integration events less so, public events least. He also observes that teams who understand the implications of publishing an event naturally publish fewer of them and put less data on them. If a stream is still too noisy, the answer is topic design — several purpose-specific topics instead of one firehose that forces subscribers to ignore 99% of what arrives.
Trade-offs And Limitations
- Eventual consistency is the price of decoupling. Everything downstream is asynchronous, which is what makes extended retries and independent failure handling possible, but it also means the system is never instantaneously consistent across domains.
- Ordering costs throughput. Explicit version-stamped ordering with an inbox check plus a unique index reintroduces queue-like serialization. Implicit ordering through domain validation avoids that but only works where a business rule naturally expresses the dependency.
- Event sourcing multiplies the learning curve. The six-month ramp-up Tacey-Green reports was in a space combining event sourcing with event-driven architecture. This is his team's experience, not an industry benchmark, but it is a reason to keep the two decisions separate.
- Event contracts are less forgiving than API contracts. Replay from the beginning of the stream means old consumers of old events remain a live concern, so versioning has to survive indefinitely rather than until the last caller migrates.
- Inbox lookups cost something. Where that cost is prohibitive, downstream idempotency is the fallback, and it depends on every downstream actually honoring idempotency keys. Tacey-Green's advice is to do both if you can, and to accept that you are carrying risk if you do neither.
- Dead letters need an owner. The third fault-tolerance level ends with a human being woken up to decide whether an event is poisonous or replayable. That is an operational commitment, not an automated safety net.
- Proving completeness to auditors is unresolved. An attendee subject to SOX described an auditor demanding proof of completeness on the event stream — a batch-era concept applied to an infinite stream. Tacey-Green said he had not faced that question; Investec's audits have been satisfied by demonstrating an immutable, untampered log. He offered no general solution, and the exchange is worth noting precisely because it is an open problem.
Practical Takeaways
- Fix the command/event vocabulary in your team before designing anything. A command names a recipient and expects a result; an event announces a state change and expects nothing. Commands flowing through an event bus quietly remove the benefits you built it for. Decide event sourcing separately and on its own merits.
- Aim for lean events, named in business language. Carry the data that genuinely belongs to the event and nothing more: thin notification events push consumers back to your API and recreate coupling, fat events export your entity model. Business-oriented names are what turn the stream into an activity log the business can read.
- Build the outbox and inbox into your service templates on day one, not after the first incident. Transactional outbox on the publish side, ID-based deduplication on the consume side, and downstream idempotency keys where an inbox lookup would be too expensive.
- Treat every published property as permanent, and separate domain events from integration events. Review event schemas as carefully as public API schemas, carry a data version field in metadata from the start, and put the filter/aggregate/transform publisher in the shared template so nobody has to remember to protect the boundary.
- Choose your ordering strategy per use case. Prefer implicit ordering via domain validation where a business rule already expresses the dependency; reserve version stamping for cases that truly need it and accept the scalability cost and the unique-index constraint that comes with it.
- Configure all three fault-tolerance layers deliberately: in-process transient retries, broker-level backoff, then dead letter with an alert. Decide as an organization how long the middle layer may back off for, and who owns the dead letters.
- Pair enablement with delivery rather than shipping documentation, and agree estate-wide standards early — contracts, stream permissions, technology — so that consuming another team's events is not a fresh investigation each time.
Key Terms
- Event — A change in state somewhere in the system, published as a fact rather than a request.
- Command — An explicit request to a known recipient to do something, with a result expected.
- Fat / lean / thin event — Descriptions of payload size: the entire entity state, the data that genuinely pertains to the event, or a bare notification.
- Producer / consumer — A system that publishes events; a system that receives them.
- Event sourcing — Representing application state as an immutable sequence of events that must be replayed to derive current state. Distinct from, and not required by, event-driven architecture.
- Outbox pattern — Writing the state change and the outgoing event in one database transaction, with a separate dispatcher publishing from the outbox table. Prevents lost events.
- Inbox pattern — Recording a received event's ID before executing business logic, so a redelivery can be detected and skipped. Prevents duplicate processing.
- At-least-once delivery — A broker guarantee that a message will arrive, but possibly more than once; the reason consumers need deduplication.
- Dead letter — A destination for events that cannot be processed after retries, providing an escape route for poisonous messages.
- Poisonous event — An event that breaks the contract or carries unprocessable data, and would otherwise retry forever.
- Domain event — An event internal to a bounded context, free to use verbose internal language.
- Integration event — An event deliberately modeled for crossing domain boundaries, produced by filtering, aggregating, and transforming domain events.
- Public event — Investec's third tier: platform-level events published to the wider organization, subject to the same boundary protection.
- Data version property — Event metadata identifying the contract version, so consumers can branch between v1 and v2 handling during replay.
- Aggregate — The entity whose event sequence is being ordered, such as a single shopping cart or payment.
- Idempotency key — A caller-supplied identifier allowing a downstream service to recognize and ignore a repeated request; the fallback when an inbox is too expensive.
- PSD2 — Cited by the speaker as an example of the regulation payment systems must satisfy. Supplementary: it is the EU's second Payment Services Directive.
- ACL — The speaker noted only that the integration event publisher resembles an ACL. Supplementary: in domain-driven design an anti-corruption layer is a translation boundary that stops external models leaking into your domain.
The through-line of the talk is that none of these patterns is exotic. The outbox and inbox are a table and an ID check; versioning is a metadata field and a branch. What is genuinely hard is making sure that every team gets them by default, before the first payment goes missing — which is why Tacey-Green spends as much time on developer platforms and training as on the patterns themselves.
Reference: Chris Tacey-Green, Event-Driven Patterns for Cloud-Native Banking - What Works, What Hurts?, InfoQ Dev Summit Munich, published April 20, 2026.