Three Ways to Handle Multiple Event Types in a Kafka Topic Using .NET, Avro, and Schema Registry
Sometimes you need to publish multiple event types to the same Kafka topic. While creating one topic per event type is often the simplest…

Three Ways to Handle Multiple Event Types in a Kafka Topic Using .NET, Avro, and Schema Registry
Sometimes you need to publish multiple event types to the same Kafka topic. While creating one topic per event type is often the simplest approach, some systems prefer grouping related events into a single topic. This can simplify topic management, preserve ordering guarantees at the aggregate level, and provide consumers with a unified stream of domain events.
This pattern is discussed in depth in Confluent’s article about multiple event types in a single Kafka topic.
While researching how to implement this pattern using .NET, Avro, and Schema Registry, I found that most examples and documentation are Java-focused. Practical .NET examples are relatively scarce.
This article demonstrates three different approaches for publishing and consuming multiple event types in a single Kafka topic using the Confluent .NET client and Avro Schema Registry.
The complete source code for all examples is available on GitHub.
Environment Setup
To support these examples, I created a complete Docker-based environment containing:
- Apache Kafka
- Confluent Schema Registry
- Pre-registered Avro schemas
During the Docker Compose startup process, an initialization container registers all required schemas in Schema Registry.
Because all schemas are registered during environment initialization, the examples disable automatic schema registration and instruct the serializer to use the latest registered version:
AutoRegisterSchemas = false;
UseLatestVersion = true;
The examples are based on a simple bank account domain with the following events:
- AccountOpened
- MoneyDeposited
- MoneyWithdrawn
- AccountBlocked
Generating C# Classes from Avro Schemas
To generate C# classes from Avro schemas, we’ll use the avrogen tool:
dotnet tool install --global Apache.Avro.Tools --version 1.12.1
Once installed, generate the C# model from a schema file:
avrogen -s AccountOpened.avsc .
This command generates a C# class implementing the ISpecificRecord interface. These generated classes can be used directly with Confluent's Avro serializer and deserializer implementations.
Strategy Comparison
Before diving into the implementations, here’s a quick summary of the trade-offs:
+---------------------------+----------+------------+-------------------------+
| Feature | Envelope | Avro Union | TopicRecordNameStrategy |
+---------------------------+----------+------------+-------------------------+
| Strong typing | Yes | No | Yes |
| Automatic deserialization | Yes | No | Yes* |
| Independent evolution | No | Yes | Yes |
| Schema isolation | Low | Medium | High |
| Additional libraries | No | No | Yes |
| Complexity | Low | Medium | Medium |
+---------------------------+----------+------------+-------------------------+
* Requires multi-schema-avro-deserializer.
1. Envelope Pattern
The first approach wraps all events inside a single Avro envelope record named BankAccountEvent.
This approach works well with Confluent’s standard Avro serializer and deserializer because all messages share the same top-level schema.
The example uses the TopicNameStrategy subject naming strategy, which means the value subject name follows the pattern:
<topic-name>-value
For example, if the topic name is:
multi-event-topic-envelope-pattern
the corresponding Schema Registry subject becomes:
multi-event-topic-envelope-pattern-value
The main advantage of this approach is its simplicity. However, all event types become coupled to the same envelope schema, which can make schema evolution more challenging as the number of event types grows.
The complete example can be found in the Envelope Pattern project on GitHub.
When to use this approach
This approach is ideal when simplicity is more important than schema independence and the set of event types is expected to evolve together.
2. Avro Union
The second approach uses an Avro Union to define all event types supported by the topic.
[
"DotNet.Kafka.MultiEventTopic.AccountBlocked",
"DotNet.Kafka.MultiEventTopic.AccountOpened",
"DotNet.Kafka.MultiEventTopic.MoneyDeposited",
"DotNet.Kafka.MultiEventTopic.MoneyWithdrawn"
]
This implementation uses Confluent’s GenericRecord class, allowing multiple event schemas to be serialized and deserialized through the same contract.
One significant advantage of this strategy is that the topic contract explicitly defines which event types are allowed. It also allows each event schema to evolve independently while maintaining a well-defined topic-level contract.
This approach closely follows the Avro Union pattern described by Confluent.
The trade-off is that consumers typically need to map GenericRecord instances back to strongly typed domain models.
The complete example can be found in the Avro Union project on GitHub.
When to use this approach
This approach is a good choice when you want a clearly defined topic contract while still allowing independent schema evolution for each event type.
3. TopicRecordNameStrategy
The third approach uses the TopicRecordNameStrategy subject naming strategy.
With this strategy, the subject name is composed of:
<topic-name>-<record-name>
For example, given the topic:
multi-event-topic-record-name-strategy
and the event:
DotNet.Kafka.MultiEventTopic.AccountOpened
the resulting subject becomes:
multi-event-topic-record-name-strategy-DotNet.Kafka.MultiEventTopic.AccountOpened
This approach allows each event type to have its own independent subject and schema evolution lifecycle.
To support automatic deserialization of multiple event types, this example uses the multi-schema-avro-deserializer library, which is also referenced in Confluent's list of third-party libraries.
using var consumer = new ConsumerBuilder<string, ISpecificRecord>(config)
.SetValueDeserializer(new MultiSchemaAvroDeserializer(schemaRegistry, avroDeserializerConfig).AsSyncOverAsync())
.Build();
...
One of the biggest advantages of this strategy is schema isolation. Changes to one event schema cannot accidentally impact unrelated event types, which can happen with the Envelope Pattern.
The complete example can be found in the TopicRecordNameStrategy project on GitHub.
When to use this approach
This approach is a good fit when you want independent schema evolution and automatic deserialization into strongly typed models.
Final Thoughts
All three approaches are valid and can be successfully used in production systems. The best choice depends on your requirements regarding schema governance, evolution, and operational complexity.
The Envelope Pattern is the simplest option and works well when event types evolve together. However, it introduces tighter coupling because all events share the same top-level schema.
The Avro Union approach provides a clear topic-level contract and explicit constraints on which event types can be published. It also allows independent schema evolution, but requires additional mapping between GenericRecord instances and strongly typed domain models.
The TopicRecordNameStrategy approach offers the greatest schema independence because each event type has its own subject. Combined with automatic deserialization, it provides a clean developer experience, although it requires an additional deserialization library.
If I were starting a new project today, I would typically choose either the Avro Union approach or the TopicRecordNameStrategy approach depending on whether I wanted stronger topic-level constraints or greater schema independence.
메타데이터
- post_id
- a0d145cace78
- slug
- three-ways-to-handle-multiple-event-types-in-a-kafka-topic-using-net-avro-and-schema-registry-a0d145cace78
- url
- https://medium.com/@leonardomagalhaes.leo/three-ways-to-handle-multiple-event-types-in-a-kafka-topic-using-net-avro-and-schema-registry-a0d145cace78
- canonical_url
- https://medium.com/@leonardomagalhaes.leo/three-ways-to-handle-multiple-event-types-in-a-kafka-topic-using-net-avro-and-schema-registry-a0d145cace78
- author_url
- https://medium.com/@leonardomagalhaes.leo
- status
- ok
- fetched_at
- 2026-06-15 20:49:13