Documentation
¶
Overview ¶
Package cqrs defines the seam between event-sourced domain entities and the infrastructure they happen to run on.
An entity in this architecture has three collaborators, and nothing else:
Publisher — the command side. Appends domain events to the event stream. Writer — the projection side. Applies statements that build a read model. Reader — the query side. Reads the read model back out.
Entities depend on these interfaces only. They never name a broker, a driver, or a database handle. That is what makes them portable between repositories and trivially testable.
Implementations ¶
The concrete adapters live in sub-packages, so everything that names a specific technology sits below this one and nothing above it has to:
sqlpersister — Writer that executes statements against a database.
deadletter — Writer decorator that diverts failing statements to a
table instead of letting them kill the projection loop.
cqrstest — in-memory Writer and Publisher for tests.
The production Publisher and Reader have no adapter here: a metatagger over JetStream and an *sql.DB respectively satisfy the interfaces as they stand.
Relationship to the stream package ¶
Message, MutableMessage, MessageFunc and Subject are aliases for the corresponding github.com/jrgensen/stream types, and Publisher and Consumer are declared with method sets identical to stream.Publisher and stream.Consumer. Aliases rather than wrappers, deliberately: the existing jetstream transport and the streamtest fakes satisfy these interfaces implicitly, so no adapter layer exists to go stale. The compile-time assertions at the bottom of this file keep both directions honest.
The aliases are the one place that names a concrete messaging library. When the event transport is replaced, this file changes and no entity does.
Example ¶
package main
import (
"context"
"fmt"
"github.com/jrgensen/cqrs"
"github.com/jrgensen/cqrs/cqrstest"
)
// This example is the shape every entity in this architecture takes: a command
// side that turns a decision into an event, and a projection side that folds
// that event into the read model. Neither names a broker or a driver — only the
// cqrs interfaces — which is what lets the whole round trip be exercised here
// with the in-memory fakes from cqrstest.
//
// The query side is omitted for brevity; it is a plain struct holding a
// cqrs.Reader, which *sql.DB satisfies directly.
// renamed is the event body.
type renamed struct {
TeamID string `json:"teamId"`
Name string `json:"name"`
}
type team struct {
p cqrs.Publisher
w cqrs.Writer
}
// Rename is the command side. Messages are not constructed directly: ask the
// Publisher for one bound to a subject, so it takes whatever form the transport
// needs.
func (t *team) Rename(ctx context.Context, teamID, name string) error {
msg := t.p.MessageFunc()(cqrs.SubjectFromStr(
fmt.Sprintf("NATHEJK:2026.patrulje.%s.updated", teamID)))
if err := msg.SetBody(&renamed{TeamID: teamID, Name: name}); err != nil {
return err
}
return t.p.Publish(msg)
}
// Consumes declares the subjects this projection cares about.
func (t *team) Consumes() []cqrs.Subject {
return []cqrs.Subject{cqrs.SubjectFromStr("NATHEJK:*.patrulje.*.updated")}
}
// HandleMessage is the projection side. It is idempotent, because the stream is
// replayed from sequence zero on every boot.
func (t *team) HandleMessage(msg cqrs.Message) error {
if !msg.Subject().Match("NATHEJK.*.patrulje.*.updated") {
return nil
}
var body renamed
if err := msg.Body(&body); err != nil {
return err
}
return t.w.Consume(fmt.Sprintf(
"INSERT INTO patrulje SET teamId=%q, name=%q"+
" ON DUPLICATE KEY UPDATE name=VALUES(name)", body.TeamID, body.Name))
}
var _ cqrs.Consumer = (*team)(nil)
func main() {
pub, w := &cqrstest.Publisher{}, &cqrstest.Writer{}
t := &team{p: pub, w: w}
if err := t.Rename(context.Background(), "abc123", "Ulven"); err != nil {
fmt.Println("rename:", err)
return
}
fmt.Println(pub.Subjects()[0])
// Feed the published events back in, as the stream mux would.
for _, msg := range pub.Messages {
if err := t.HandleMessage(msg); err != nil {
fmt.Println("handle:", err)
return
}
}
fmt.Println(w.Last())
}
Output: NATHEJK.2026.patrulje.abc123.updated INSERT INTO patrulje SET teamId="abc123", name="Ulven" ON DUPLICATE KEY UPDATE name=VALUES(name)
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EnsureColumn ¶
EnsureColumn adds a column to a table if it doesn't already exist. It's the missing piece of the "CREATE TABLE IF NOT EXISTS" pattern the projections use: that statement creates new tables but never adds columns to existing ones.
ddl is the part of an ALTER TABLE statement that follows "ADD COLUMN", for example:
"sizes VARCHAR(255) NOT NULL DEFAULT '' AFTER eligibleFor"
The existence check goes through the Reader and the change through the Writer, matching the split everywhere else: reads return values, writes are statements the Writer may divert or defer.
The check uses INFORMATION_SCHEMA.COLUMNS, so it works on both MySQL and MariaDB without requiring a specific server version. EnsureColumn is idempotent and safe to call on every startup.
func EnsureIndex ¶
EnsureIndex adds a named index to a table if it doesn't already exist. Like EnsureColumn but for indexes; ddl is the full ALTER TABLE statement so the caller has full control over UNIQUE, FULLTEXT, composite columns, etc.:
"ALTER TABLE order_line ADD INDEX idx_order_line_member (memberId)"
Types ¶
type Consumer ¶
Consumer is the projection side of an entity: it declares the subjects it cares about and folds each matching event into the read model, normally by handing statements to a Writer.
HandleMessage must be idempotent. Projections are rebuilt by replaying the stream from the beginning on every boot, so every event will be handled again, and an event may be redelivered within a single run.
type MessageFunc ¶
type MessageFunc = stream.MessageFunc
MessageFunc builds a MutableMessage for a subject. A Publisher hands one out so callers construct messages in whatever form that particular transport requires.
type MutableMessage ¶
type MutableMessage = stream.MutableMessage
MutableMessage is an event being composed, before it is published.
type Publisher ¶
type Publisher interface {
Publish(msg Message) error
MessageFunc() MessageFunc
}
Publisher is the command side of an entity: it turns a decision into an event on the stream.
Messages are not constructed directly. Ask MessageFunc for a message bound to a subject, set its body, then Publish it:
msg := p.MessageFunc()(cqrs.SubjectFromStr("NATHEJK:2026.patrulje.abc.updated"))
msg.SetBody(&body)
return p.Publish(msg)
Publish may be synchronous or asynchronous, and must be safe for concurrent use.
type Reader ¶
type Reader interface {
Begin() (*sql.Tx, error)
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
Exec(query string, args ...interface{}) (sql.Result, error)
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}
Reader is the query side of the read model.
The method set is that of *sql.DB, minus the connection-lifecycle calls (Close, Ping, SetMaxOpenConns, Stats, ...) that belong to whoever owns the pool rather than to an entity. Two consequences worth knowing about:
- *sql.DB satisfies Reader as it stands, so no adapter is needed at the call site in main. Note that *sql.Tx does not: it has no Begin, since a transaction cannot start another one.
- Reader is a superset of goqu.SQLDatabase, so a Reader can be handed straight to goqu.New. Removing a method here will break the entities that build queries with goqu; add, don't subtract.
Mock it with a database/sql mock (github.com/DATA-DOG/go-sqlmock) rather than by hand — the *sql.Rows and *sql.Row return types are concrete and cannot be fabricated otherwise.
type Subject ¶
Subject addresses an event, e.g. "NATHEJK:2026.patrulje.abc123.updated".
func SubjectFromStr ¶
SubjectFromStr parses the canonical string form of a subject, where the domain is separated from the type by either ':' or '.':
NATHEJK:2026.patrulje.abc123.updated
It exists so entities can name subjects without importing a transport package for the constructor alone.
type Writer ¶
Writer applies a single statement to the read model.
The contract is deliberately this thin. A Writer is free to execute the statement, log it, buffer it, or divert a failing one to a dead-letter queue; callers cannot tell and must not care. Because there is no result value, a Writer is only ever used to project — never to read back.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cqrstest provides in-memory implementations of the cqrs interfaces for use in tests.
|
Package cqrstest provides in-memory implementations of the cqrs interfaces for use in tests. |
|
Package deadletter provides a cqrs.Writer decorator that acts as a safety net around another Writer.
|
Package deadletter provides a cqrs.Writer decorator that acts as a safety net around another Writer. |
|
Package sqlpersister provides the cqrs.Writer used in production: it executes each projection statement against the database, echoing any statement that fails to a log sink (stderr by default) before returning the error.
|
Package sqlpersister provides the cqrs.Writer used in production: it executes each projection statement against the database, echoing any statement that fails to a log sink (stderr by default) before returning the error. |