Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f1ac022
chore: scaffold multi-module Go project structure
alexeyzimarev Mar 20, 2026
5d8d5eb
feat(core): add shared primitives — StreamName, Metadata, errors
alexeyzimarev Mar 20, 2026
4657d95
feat(core): add TypeMap for bidirectional event type registration
alexeyzimarev Mar 20, 2026
0a07577
feat(core): add Codec interface and JSON implementation
alexeyzimarev Mar 20, 2026
80aa598
feat(core): add Aggregate[S] domain type with fold, apply, and guards
alexeyzimarev Mar 20, 2026
0825c1b
feat(core): add EventStore/Reader/Writer interfaces and stream types
alexeyzimarev Mar 20, 2026
b26d84e
feat(core): add in-memory EventStore for testing
alexeyzimarev Mar 20, 2026
717b489
feat(core): add store conformance test suite and validate with in-mem…
alexeyzimarev Mar 20, 2026
4ca4da4
feat(core): add LoadState, LoadAggregate, StoreAggregate functions
alexeyzimarev Mar 20, 2026
5e9ad85
test(core): add shared Booking test domain mirroring .NET Eventuous.S…
alexeyzimarev Mar 20, 2026
f0ae477
feat(core): add functional command service with handler registration
alexeyzimarev Mar 20, 2026
43668c4
feat(core): add aggregate command service
alexeyzimarev Mar 20, 2026
e588207
feat(core): add subscription framework with middleware chain
alexeyzimarev Mar 20, 2026
3febf56
feat(core): add checkpoint committer with gap detection
alexeyzimarev Mar 20, 2026
a50cf31
feat(core): add subscription conformance test suite
alexeyzimarev Mar 20, 2026
94c51b5
feat(kurrentdb): implement EventStore with conformance tests passing
alexeyzimarev Mar 20, 2026
af7e624
refactor(kurrentdb): use testcontainers for integration tests
alexeyzimarev Mar 20, 2026
519853f
feat(kurrentdb): add catch-up subscription for stream and $all
alexeyzimarev Mar 20, 2026
ca5aa9f
feat(kurrentdb): add persistent subscription with ack/nack
alexeyzimarev Mar 20, 2026
4e357ca
feat(otel): add command tracing and subscription tracing middleware
alexeyzimarev Mar 20, 2026
2172ba9
feat(kurrentdb): add subscription conformance tests and enhance subte…
alexeyzimarev Mar 20, 2026
cea0c85
feat(otel): add command tracing and subscription tracing middleware
alexeyzimarev Mar 20, 2026
b114804
test(kurrentdb): add end-to-end integration test
alexeyzimarev Mar 20, 2026
55c75ac
ci: add GitHub Actions CI pipeline
alexeyzimarev Mar 20, 2026
02e24d1
style: fix gofmt formatting in 6 files
alexeyzimarev Mar 20, 2026
3e84ce4
chore: add Apache 2.0 license headers to all Go files
alexeyzimarev Mar 20, 2026
21c4d89
docs: add README with quick start, architecture, and examples
alexeyzimarev Mar 20, 2026
da1f2e4
fix: address code review feedback
alexeyzimarev Mar 20, 2026
8d9d6d4
test: add regression tests for code review fixes
alexeyzimarev Mar 20, 2026
57eee27
style: fix gofmt formatting
alexeyzimarev Mar 20, 2026
272fa18
docs: flesh out CLAUDE.md with architecture, conventions, and test pa…
alexeyzimarev Mar 20, 2026
1d958e0
ci: skip pipeline for docs-only and non-code changes
alexeyzimarev Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: CI

on:
pull_request:
paths-ignore:
- "docs/**"
- "**.md"
- "LICENSE"
- ".gitignore"
push:
branches:
- main
- dev
paths-ignore:
- "docs/**"
- "**.md"
- "LICENSE"
- ".gitignore"

permissions:
contents: read

env:
GO_VERSION: "1.25"

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Vet core
run: cd core && go vet ./...

- name: Vet kurrentdb
run: cd kurrentdb && go vet ./...

- name: Vet otel
run: cd otel && go vet ./...

- name: Check formatting
run: |
unformatted=$(gofmt -l core/ kurrentdb/ otel/)
if [ -n "$unformatted" ]; then
echo "Files not formatted:"
echo "$unformatted"
exit 1
fi

build:
name: Build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: [core, kurrentdb, otel]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Build ${{ matrix.module }}
run: cd ${{ matrix.module }} && go build ./...

test-core:
name: Test core
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Run core tests
run: cd core && go test -race -count=1 ./...

test-otel:
name: Test otel
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Run otel tests
run: cd otel && go test -race -count=1 ./...

test-kurrentdb:
name: Test kurrentdb
needs: build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Run kurrentdb integration tests
run: cd kurrentdb && go test -race -count=1 -timeout 300s ./...
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Binaries
*.exe
*.dll
*.so
*.dylib

# Test artifacts
*.test
*.out
coverage.txt

# IDE
.idea/
.vscode/
*.swp

# OS
.DS_Store
149 changes: 149 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# CLAUDE.md

This file provides guidance to Claude Code when working with code in this repository.

## What Is Eventuous Go

Go port of [Eventuous](https://github.com/Eventuous/eventuous), a production-grade Event Sourcing library. Implements DDD tactical patterns for Go: aggregates, command services, event stores, subscriptions, and projections. Functional-first design — the functional command service is the primary path; aggregate-based service is an optional layer.

## Build & Test Commands

```bash
# Build all modules
(cd core && go build ./...)
(cd kurrentdb && go build ./...)
(cd otel && go build ./...)

# Run all core tests (no infrastructure needed)
(cd core && go test ./...)

# Run all core tests with race detector
(cd core && go test -race ./...)

# Run kurrentdb integration tests (uses testcontainers — Docker required)
(cd kurrentdb && go test -race -timeout 300s ./...)

# Run otel tests
(cd otel && go test -race ./...)

# Run a single test by name
(cd core && go test ./aggregate/... -run TestClearChanges_AdvancesVersion -v)

# Check formatting
gofmt -l core/ kurrentdb/ otel/

# Vet all modules
(cd core && go vet ./...) && (cd kurrentdb && go vet ./...) && (cd otel && go vet ./...)
```

Integration tests use testcontainers-go to start KurrentDB automatically — no manual `docker compose up` needed. Docker must be running.

## Module Structure

This is a multi-module Go project for dependency efficiency. Each module has its own `go.mod`.

```
core/ # github.com/eventuous/eventuous-go/core
├── eventuous.go # StreamName, ExpectedVersion, Metadata, sentinel errors
├── aggregate/ # Domain: Aggregate[S] with fold, apply, guards
├── codec/ # TypeMap (bidirectional type registry), Codec interface, JSON impl
├── store/ # EventReader/Writer/Store interfaces, LoadState, LoadAggregate, StoreAggregate
├── command/ # Functional Service[S], AggregateService[S], handler registration
├── subscription/ # EventHandler, middleware chain, CheckpointCommitter with gap detection
└── test/ # Exported test infrastructure
├── memstore/ # In-memory EventStore for unit testing
├── storetest/ # Store conformance test suite (run by any store impl)
├── subtest/ # Subscription conformance test suite
└── testdomain/ # Shared Booking domain (events, state, commands, codec setup)

kurrentdb/ # github.com/eventuous/eventuous-go/kurrentdb
├── store.go # EventStore implementation for KurrentDB
├── catchup.go # Catch-up subscription (stream + $all)
├── persistent.go # Persistent subscription (stream + $all, ack/nack)
└── options.go # Functional options for subscriptions

otel/ # github.com/eventuous/eventuous-go/otel
├── command.go # TracedCommandHandler decorator (tracing + metrics)
└── subscription.go # TracingMiddleware for subscriptions
```

Dependencies flow: `kurrentdb → core`, `otel → core`. Core depends on nothing heavy.

## Architecture

### Domain Model

`Aggregate[S]` tracks state, pending changes, and version. State is any struct — no interface needed. State reconstruction uses a **fold function** (`func(S, any) S`) with a type switch, not handler registration.

### Command Services (Two Approaches)

**Functional** (primary): `command.Service[S]` — loads state via fold, handler is `func(ctx, state, cmd) ([]any, error)`, no aggregate involved. Registered via `command.On(svc, handler)`.

**Aggregate** (optional): `command.AggregateService[S]` — loads aggregate, handler calls `agg.Apply()`, framework reads `agg.Changes()`. Registered via `command.OnAggregate(svc, handler)`.

### Persistence

`store.EventReader`, `store.EventWriter`, `store.EventStore` interfaces. Package-level generic functions `LoadState`, `LoadAggregate`, `StoreAggregate` handle the load/store cycle.

### Subscriptions

`subscription.EventHandler` interface with `HandlerFunc` adaptor. Middleware chain pattern (like `net/http`): `WithConcurrency`, `WithPartitioning`, `WithLogging`. `CheckpointCommitter` handles batched commits with gap detection.

### Serialization

`codec.TypeMap` for bidirectional type name mapping (explicit registration, no reflection magic). `codec.Codec` interface with `JSONCodec` implementation.

## Key Conventions

- **Package name**: the root package is `eventuous` (import as `eventuous "github.com/eventuous/eventuous-go/core"`)
- **Stream naming**: `{Category}-{ID}` via `eventuous.NewStreamName(category, id)`
- **Type mapping**: events must be explicitly registered in `codec.TypeMap` — `codec.Register[MyEvent](tm, "MyEvent")`
- **Errors**: sentinel errors with `errors.Is()` — `ErrStreamNotFound`, `ErrOptimisticConcurrency`, `ErrHandlerNotFound`
- **Context**: all I/O functions take `context.Context` as first parameter
- **No DI container**: explicit wiring, functional options for configuration

## Code Style

- Go 1.25+ (matches module requirements in go.mod)
- Use generics where they reduce boilerplate, not for everything
- Errors over panics. Sentinel errors with `errors.Is()`
- `context.Context` as first parameter on all I/O functions
- `slog` for structured logging
- Functional options for configuration (subscription options, etc.)
- Table-driven tests
- All `.go` files must have the license header:
```go
// Copyright (C) Eventuous HQ OÜ. All rights reserved
// Licensed under the Apache License, Version 2.0.
```
- Run `gofmt` before committing — CI enforces formatting
- Run `go vet` — CI enforces vet

## Test Patterns

### Conformance test suites

Store implementations run the shared conformance suite:
```go
func TestMyStore(t *testing.T) {
s := mystore.New()
storetest.RunAll(t, s)
}
```

### Shared test domain

All command service and subscription tests use the Booking domain from `core/test/testdomain/`:
- Events: `RoomBooked`, `BookingImported`, `PaymentRecorded`, `BookingCancelled`
- State: `BookingState` with `BookingFold`
- Commands: `BookRoom`, `ImportBooking`, `RecordPayment`, `CancelBooking`
- Helpers: `testdomain.NewCodec()`, `testdomain.BookingStream(id)`

### Integration tests

KurrentDB tests use testcontainers-go — `setupClient(t)` in `kurrentdb/testutil_test.go` starts a container automatically.

## Design Specs

- Design spec: `docs/specs/2026-03-20-phase1-design.md`
- Implementation plan: `docs/specs/2026-03-20-phase1-plan.md`
Loading
Loading