Events
Domain events are the core building block of Event Sourcing. They represent facts — things that happened in your domain. Once stored, events are immutable and permanent.
What are domain events?
Section titled “What are domain events?”An event records a state change that already occurred. RoomBooked means a room was booked. BookingCancelled means a booking was cancelled. Events are always named in the past tense because they describe history.
Events differ from commands: a command is a request that might be rejected (“please book this room”), while an event is a fact that already happened (“the room was booked”). Commands are input, events are output.
In Event Sourcing, events are the source of truth. The current state of any entity is derived by replaying its events. Events are also used to communicate between parts of the system — subscriptions react to events for projections, notifications, and cross-system integration.
Events are plain Go structs
Section titled “Events are plain Go structs”Eventuous Go does not require events to implement any interface or embed any base type. Events are simple structs:
type RoomBooked struct { BookingID string `json:"bookingId"` RoomID string `json:"roomId"` CheckIn string `json:"checkIn"` CheckOut string `json:"checkOut"` Price float64 `json:"price"`}
type PaymentRecorded struct { BookingID string `json:"bookingId"` Amount float64 `json:"amount"`}
type BookingCancelled struct { BookingID string `json:"bookingId"` Reason string `json:"reason"`}JSON tags
Section titled “JSON tags”Always add JSON tags to your event fields. Events are serialized to JSON for storage and the tags control the field names in the JSON representation. Without tags, Go uses the uppercase field names (BookingID instead of bookingId), which is unconventional for JSON and can cause interoperability issues if you have consumers written in other languages.
Type registration
Section titled “Type registration”Every event type must be registered in a TypeMap with a stable string name before it can be serialized or deserialized.
import "github.com/eventuous/eventuous-go/core/codec"
types := codec.NewTypeMap()codec.Register[RoomBooked](types, "RoomBooked")codec.Register[PaymentRecorded](types, "PaymentRecorded")codec.Register[BookingCancelled](types, "BookingCancelled")Why explicit type names?
Section titled “Why explicit type names?”The string name "RoomBooked" is what gets written to the event store alongside the event data. This is the persistent identity of the event type. It must remain stable forever.
If you rename the Go struct from RoomBooked to HotelRoomBooked, the stored events still have "RoomBooked" as their type name. The TypeMap maps the new Go struct to the old name, so deserialization continues to work:
// Even after renaming the struct, the stored name stays the samecodec.Register[HotelRoomBooked](types, "RoomBooked")Without explicit names, the system would have to derive names from Go types using reflection. That would mean renaming a struct is a breaking change that corrupts your ability to read existing events. Explicit registration prevents this class of bugs entirely.
Idempotency and conflict detection
Section titled “Idempotency and conflict detection”Registering the same type with the same name is idempotent — calling Register twice with the same arguments does nothing. However, registering the same type with a different name, or a different type with the same name, returns an error:
// OK: idempotentcodec.Register[RoomBooked](types, "RoomBooked")codec.Register[RoomBooked](types, "RoomBooked") // no error
// Error: type already registered under a different namecodec.Register[RoomBooked](types, "DifferentName")
// Error: name already registered for a different typecodec.Register[SomeOtherEvent](types, "RoomBooked")Thread safety
Section titled “Thread safety”The TypeMap is safe for concurrent use. It uses a sync.RWMutex internally, so you can register types from multiple goroutines during initialization without additional synchronization.
The Codec interface
Section titled “The Codec interface”The Codec interface handles encoding events to bytes and decoding bytes back to events:
type Codec interface { Encode(event any) (data []byte, eventType string, contentType string, err error) Decode(data []byte, eventType string) (event any, err error)}- Encode takes an event struct, returns the serialized bytes, the registered type name, and the content type (e.g.,
"application/json"). - Decode takes serialized bytes and the type name, returns the deserialized event struct.
JSON codec
Section titled “JSON codec”The built-in JSON codec uses encoding/json and a TypeMap:
jsonCodec := codec.NewJSON(types)The encoding flow:
- Look up the event’s Go type in the TypeMap to get the string name
- Marshal the event to JSON using
json.Marshal - Return the JSON bytes, type name, and
"application/json"
The decoding flow:
- Look up the string name in the TypeMap to find the Go type
- Create a new zero-value instance of that type
- Unmarshal the JSON bytes into it using
json.Unmarshal - Return the deserialized event as a value (not a pointer)
Custom codecs
Section titled “Custom codecs”If you need a different serialization format (Protocol Buffers, MessagePack, etc.), implement the Codec interface. You still need a TypeMap for type resolution, but the encoding/decoding is your own logic.
Best practice: one registration function per bounded context
Section titled “Best practice: one registration function per bounded context”Group all event registrations for a bounded context in a single function. This makes it easy to find all event types, prevents accidental omissions, and gives you a single call site for initialization:
func RegisterBookingEvents(tm *codec.TypeMap) { must(codec.Register[RoomBooked](tm, "RoomBooked")) must(codec.Register[PaymentRecorded](tm, "PaymentRecorded")) must(codec.Register[BookingCancelled](tm, "BookingCancelled"))}
func must(err error) { if err != nil { panic(err) }}Call this at application startup, before creating the codec or any services:
types := codec.NewTypeMap()RegisterBookingEvents(types)jsonCodec := codec.NewJSON(types)