Appearance
Serialization & Codecs
A fundamental design philosophy of flux is that the core domain must remain completely unaware of how it is serialized.
If you look at the flux.Envelope or your own Domain Events (e.g., ProductCreated), you will not find a single json:, xml:, or bson: struct tag. Keeping these infrastructure concerns out of your domain prevents vendor lock-in and allows your system to seamlessly adapt to any database driver or message broker.
So, how do backend drivers (like MySQL, Redis, or Kafka) actually serialize an envelope to bytes?
They use the built-in Type Registry and Serialization Codecs.
The Challenge: Deserializing Interfaces
The flux.Envelope struct contains the inner domain event as a Go interface: Event flux.Event.
Because it is an interface, standard Go functions like json.Unmarshal() or xml.Unmarshal() will fail to decode it. The Go compiler has no way of knowing whether the raw JSON bytes represent a ProductCreated struct or an OrderPlaced struct.
To solve this, a framework must provide a Type Registry that converts a string name back into a concrete Go pointer.
1. The Generic Type Registry (event.Types)
flux solves the interface deserialization problem natively, without using Go's slow reflect package.
Instead, it relies on Go Generics to provide a blazing-fast, type-safe registry in the flux/event package.
Registering Events
During your application's startup, you register your events with the generic Types registry. This tells the framework how to instantiate empty pointers of your events.
go
import (
"github.com/wotek/flux/event"
"e-commerce/internal/catalog/events"
)
// Create a new registry
registry := event.NewTypes()
// Register your events using pure Generics!
event.RegisterType[events.ProductCreated](registry)
event.RegisterType[events.PriceUpdated](registry)Registering Pointer Receivers (The Pointer Problem)
The standard event.RegisterType[T] function expects a value type (e.g., events.ProductCreated). It guarantees that new(T) generates a safe, single pointer.
However, certain libraries—most notably Protocol Buffers (protoc)—generate Go code where the required interface methods are attached strictly to the pointer receiver.
go
// Generated by protoc. The value type does NOT implement proto.Message!
func (x *ProductCreated) ProtoReflect() protoreflect.Message { ... }Because the value type does not fulfill the interface, you cannot pass it to RegisterType. And if you attempt to pass the pointer type (event.RegisterType[*events.ProductCreated]), Go will generate a double pointer (**ProductCreated), which instantly crashes the unmarshaler!
To solve this, flux provides RegisterPointerType. It uses advanced Go Generics to allow you to pass the base value type, while strictly enforcing that its pointer implements the flux.Event interface:
go
registry := event.NewTypes()
// ❌ PANICS at startup! You cannot pass a pointer to RegisterType.
event.RegisterType[*events.ProductCreated](registry)
// ❌ COMPILE ERROR! The value type doesn't implement proto.Message.
event.RegisterType[events.ProductCreated](registry)
// ✅ CORRECT! Pass the value type to RegisterPointerType.
// The compiler guarantees the *pointer* implements the interface, and safely returns a single pointer!
event.RegisterPointerType[events.ProductCreated](registry)Instantiating Events (For Backend Drivers)
If you are writing a custom database driver, you simply ask the registry for an empty pointer using the event's name.
go
// 1. Get an empty *events.ProductCreated pointer wrapped in the Event interface
eventPtr, err := registry.Instantiate("ProductCreated")
// 2. Unmarshal raw JSON bytes directly into the interface value!
// Go is smart enough to unpack the interface and populate the underlying pointer.
json.Unmarshal(rawBytes, eventPtr)
// 3. Assign it safely back to the Envelope
envelope.Event = eventPtr2. Serialization Codecs
To save backend driver authors from reinventing the wheel, flux provides "Batteries-Included" codecs. These codecs handle the complex mapping of the flux.Envelope into private Data Transfer Objects (DTOs) safely.
The Codec Interface
All codecs implement a dead-simple interface:
go
package codec
type Serializer interface {
Marshal(env flux.Envelope) ([]byte, error)
Unmarshal(data []byte) (flux.Envelope, error)
}Supported Codecs
flux currently ships with two natively supported codecs: JSON and XML.
Using the JSON Codec
The JSON codec provides standard, human-readable serialization. It is perfect for logging, REST APIs, or document databases like MongoDB and PostgreSQL JSONB columns.
go
import (
"github.com/wotek/flux/codec/json"
"github.com/wotek/flux/event"
)
// Initialize your registry
registry := event.NewTypes()
event.RegisterType[ProductCreated](registry)
// Initialize the codec
jsonCodec := json.New(registry)
// Marshal an envelope to bytes
bytes, _ := jsonCodec.Marshal(myEnvelope)
// Unmarshal bytes back into an envelope
rebuiltEnv, _ := jsonCodec.Unmarshal(bytes)Using the XML Codec
The XML codec behaves identically but outputs strict XML. It securely maps map metadata into <entry> blocks and uses <innerxml> wrapping to serialize the domain event safely.
go
import "github.com/wotek/flux/codec/xml"
xmlCodec := xml.New(registry)
bytes, _ := xmlCodec.Marshal(myEnvelope)Using the Protocol Buffers Codec
For enterprise environments requiring massive scale, cross-language support, and strict schema versioning, flux provides a native Protocol Buffers (Protobuf) codec.
The Protobuf Contract: Unlike JSON or XML which use reflection to serialize arbitrary Go structs, the Protobuf Go library requires structs to explicitly implement the proto.Message interface. Therefore, if you choose the Protobuf codec, all of your Domain Events must be defined as .proto messages and compiled via protoc.
protobuf
// events.proto
syntax = "proto3";
package ecommerce.events;
option go_package = "e-commerce/internal/catalog/events";
message ProductCreated {
string name = 1;
int64 price = 2;
}Because protoc generates Go structs where the methods are attached to the pointer receiver (e.g., func (*ProductCreated) Name() string), the base value type does not implement flux.Event. To register Protobuf events into the flux registry, you must use the specialized RegisterPointerType function:
go
import (
"github.com/wotek/flux/codec/protobuf"
"github.com/wotek/flux/event"
)
registry := event.NewTypes()
// Use RegisterPointerType for compiled Protobuf messages!
event.RegisterPointerType[events.ProductCreated](registry)
// Initialize the codec
protoCodec := protobuf.New(registry)
// Marshal an envelope to highly compressed binary bytes
bytes, _ := protoCodec.Marshal(myEnvelope)Writing Your Own Codec
If your enterprise requires extreme performance with exotic formats, you can easily write your own MessagePackSerializer or AvroSerializer.
Simply define a struct that implements codec.Serializer, pass the event.Types registry into its constructor, and provide it to your backend driver!
