Learn / Tutorials / First Steps with ArchitectureKit / Defining Events and State
Step 2 of 6

Defining Events and State

In this step, you define the events of a book, the state that is built from them, and register the schemas of the events with EventSourcingDB.

Defining Events#

An event describes something that has happened. In the library, a book goes through three of them: it is acquired, borrowed, and returned.

In ArchitectureKit, an event is a struct with JSON annotations that implements two functions: EventType, which returns the event type, and Schema, which describes the event's data as a JSON schema. Create a file events.go:

package main

type BookAcquired struct {
  Title  string `json:"title"`
  Author string `json:"author"`
  ISBN   string `json:"isbn"`
}

func (BookAcquired) EventType() string {
  return "io.eventsourcingdb.library.book-acquired"
}

func (BookAcquired) Schema() map[string]any {
  return map[string]any{
    "type": "object",
    "properties": map[string]any{
      "title":  map[string]any{"type": "string"},
      "author": map[string]any{"type": "string"},
      "isbn":   map[string]any{"type": "string"},
    },
    "required":             []string{"title", "author", "isbn"},
    "additionalProperties": false,
  }
}

type BookBorrowed struct {
  BorrowedBy    string `json:"borrowedBy"`
  BorrowedUntil string `json:"borrowedUntil"`
}

func (BookBorrowed) EventType() string {
  return "io.eventsourcingdb.library.book-borrowed"
}

func (BookBorrowed) Schema() map[string]any {
  return map[string]any{
    "type": "object",
    "properties": map[string]any{
      "borrowedBy":    map[string]any{"type": "string"},
      "borrowedUntil": map[string]any{"type": "string", "format": "date"},
    },
    "required":             []string{"borrowedBy", "borrowedUntil"},
    "additionalProperties": false,
  }
}

type BookReturned struct{}

func (BookReturned) EventType() string {
  return "io.eventsourcingdb.library.book-returned"
}

func (BookReturned) Schema() map[string]any {
  return map[string]any{
    "type":                 "object",
    "properties":           map[string]any{},
    "additionalProperties": false,
  }
}

The struct becomes the data of the event. Other parts of an event are taken from elsewhere: the subject, which names the book an event belongs to, comes from the command that causes it, and the source from the store that you create below. The event types are written as reverse domain names, as EventSourcingDB requires (see Event Types).

BookReturned carries no data, so its schema describes an empty object. The schema of BookBorrowed requires the return date to be a date, such as 2026-10-24.

The schema is not optional: an event without a Schema function does not compile. For details, see Defining Events.

Defining the State#

To decide whether a book can be borrowed, you need to know whether it exists and whether it is borrowed right now. That is the state of a book, and it is built from the book's events.

Create a file book.go:

package main

import "github.com/thenativeweb/architecturekit-golang/architecturekit"

type Book struct {
  IsAcquired bool
  IsBorrowed bool
}

var bookState = architecturekit.NewState(Book{}).
  Evolve(func(book Book, event BookAcquired) Book {
    book.IsAcquired = true
    return book
  }).
  Evolve(func(book Book, event BookBorrowed) Book {
    book.IsBorrowed = true
    return book
  }).
  Evolve(func(book Book, event BookReturned) Book {
    book.IsBorrowed = false
    return book
  })

NewState takes the initial value, and every call to Evolve says how an event changes the state. The event type is taken from the event's EventType function, so it does not have to be repeated.

Note that the state holds only what decisions need: neither the title nor the author of a book decides whether it can be borrowed, so the state leaves them out. Listing books with their titles is a job for the reading side, which you build in Defining a View and a Query. For details, see Defining State.

Registering the Schemas#

EventSourcingDB checks events against a schema only once the schema is registered. The state knows the schemas of all events it evolves by, and the store, which reads events from and writes them to EventSourcingDB, registers them.

Create a file store.go that connects to EventSourcingDB, creates the store, and registers the schemas:

package main

import (
  "net/url"

  "github.com/thenativeweb/architecturekit-golang/architecturekit"
  "github.com/thenativeweb/eventsourcingdb-client-golang/eventsourcingdb"
)

func newStore() (*architecturekit.Store, error) {
  baseURL, err := url.Parse("http://localhost:3000")
  if err != nil {
    return nil, err
  }

  client, err := eventsourcingdb.NewClient(baseURL, "secret")
  if err != nil {
    return nil, err
  }

  store := architecturekit.NewStore(client, "https://library.eventsourcingdb.io")

  err = store.RegisterSchemas(bookState.Schemas())
  if err != nil {
    return nil, err
  }

  return store, nil
}

The second argument of NewStore is the source of all events that the store writes (see Sources). Schemas returns the schemas of all events the state evolves by, and RegisterSchemas registers them with EventSourcingDB.

To try it, create a file main.go:

package main

import (
  "fmt"
  "log"
)

func main() {
  _, err := newStore()
  if err != nil {
    log.Fatal(err)
  }

  fmt.Println("Registered the event schemas.")
}

Run the program:

go run .

It prints:

Registered the event schemas.

Run it a second time. The output is the same, because RegisterSchemas is meant to be called on every start: for an event type that EventSourcingDB knows already, it checks that the registered schema is exactly the one from the code.

To see what EventSourcingDB knows now, list its event types:

curl \
  -X POST \
  -H "authorization: Bearer secret" \
  http://localhost:3000/api/v1/read-event-types

The response contains the three event types, each with its schema:

{"type":"eventType","payload":{"eventType":"io.eventsourcingdb.library.book-acquired","isPhantom":true,"schema":{"additionalProperties":false,"properties":{"author":{"type":"string"},"isbn":{"type":"string"},"title":{"type":"string"}},"required":["title","author","isbn"],"type":"object"}}}
{"type":"eventType","payload":{"eventType":"io.eventsourcingdb.library.book-borrowed","isPhantom":true,"schema":{"additionalProperties":false,"properties":{"borrowedBy":{"type":"string"},"borrowedUntil":{"format":"date","type":"string"}},"required":["borrowedBy","borrowedUntil"],"type":"object"}}}
{"type":"eventType","payload":{"eventType":"io.eventsourcingdb.library.book-returned","isPhantom":true,"schema":{"additionalProperties":false,"properties":{},"type":"object"}}}

All three are phantoms, since no events of these types have been written yet (see A Word on Phantom Event Types).

Keeping Schemas Stable#

A registered schema cannot change. Suppose you added a property pages to the schema of BookAcquired – the program would then refuse to start, with a message that begins with the current date and time:

2026/09/29 21:57:37 architecturekit: permanent failure: the schema of "io.eventsourcingdb.library.book-acquired" differs from the registered one, which cannot change; introduce a new event type and an upcaster instead
exit status 1

So leave the schemas as they are for this tutorial. To change the shape of an event in a real application, introduce a new event type, and translate the stored events of the old type with an upcaster (see Versioning Events). For details on schemas, see Registering Event Schemas.