Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
958fd78
feat(examples): scaffold todolist proto contracts
ar3s3ru Apr 21, 2026
e4f02b5
feat(examples): generate connect stubs for todolist
ar3s3ru Apr 21, 2026
719c9d4
feat(examples): port todolist domain aggregate
ar3s3ru Apr 21, 2026
c7bad3d
feat(examples): port todolist command handlers
ar3s3ru Apr 21, 2026
912405e
feat(examples): port todolist query handler
ar3s3ru Apr 21, 2026
2c34ae2
feat(examples): add connect server implementation
ar3s3ru Apr 21, 2026
51f4460
feat(examples): wire todolist main binary
ar3s3ru Apr 21, 2026
d351e06
chore(lint): remove examples$ exclusion, lint the todolist example
ar3s3ru Apr 21, 2026
25803e5
docs(examples): add todolist README and link from root
ar3s3ru Apr 21, 2026
bd632f0
refactor(examples): colocate request/response messages with service
ar3s3ru Apr 21, 2026
823bdb2
chore(examples): migrate buf config to v2
ar3s3ru Apr 21, 2026
3e2c0f1
refactor(examples): package todolist internals by domain
ar3s3ru Apr 21, 2026
1c132df
refactor(examples): swap zap for slog in the todolist main
ar3s3ru Apr 21, 2026
63fbeca
refactor(examples): fold connect + protoconv into todolist package
ar3s3ru Apr 21, 2026
1dd1d81
feat(examples): add mark-as-done, mark-as-pending, delete item commands
ar3s3ru Apr 21, 2026
6e53061
feat(examples): wire new commands into ConnectServiceHandler
ar3s3ru Apr 21, 2026
266ff1d
ci(examples): run lint and tests across all Go modules
ar3s3ru Apr 21, 2026
b01b14a
ci(examples): introduce Go workspace, drop replace directive
ar3s3ru Apr 21, 2026
820482c
ci: exclude examples and generated code from codecov metrics
ar3s3ru Apr 21, 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
7 changes: 7 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
# Codecov configuration.
# https://docs.codecov.com/docs/codecov-yaml

ignore:
- "examples/**"
- "**/gen/**"
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.txt
# One coverage file per Go workspace member (see go.work).
# Keep this list in sync with go.work's use directives.
files: ./coverage.txt,./examples/todolist/coverage.txt
verbose: true
fail_ci_if_error: true
2 changes: 0 additions & 2 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ linters:
paths:
- third_party$
- builtin$
- examples$

issues:
max-issues-per-linter: 0
Expand All @@ -153,4 +152,3 @@ formatters:
paths:
- third_party$
- builtin$
- examples$
37 changes: 30 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,40 @@ endif
GOLANGCI_LINT_FLAGS ?= -v
GO_TEST_FLAGS ?= -v -cover -covermode=atomic -coverprofile=coverage.txt -coverpkg=./...

# GO_MODULES is the list of directories participating in the Go workspace,
# discovered via `go work edit -json` so go.work stays the single source of
# truth. golangci-lint and go test don't span workspace modules on their own
# (see https://github.com/golang/vscode-go/issues/2666), so we iterate.
GO_MODULES := $(shell go work edit -json | jq -r '.Use[].DiskPath')

# GOLANGCI_LINT_CONFIG points each nested module's linter at the root config.
# Every module is linted with the same rules we apply to the library.
GOLANGCI_LINT_CONFIG := $(abspath .golangci.yaml)

# run_in_modules runs a shell command in each Go workspace module. The first
# argument is a human-readable label for logs; the second is the command
# itself, which runs with $$mod pointing at the module directory.
define run_in_modules
set -e; \
for mod in $(GO_MODULES); do \
echo "==> $(1) ($$mod)"; \
( cd "$$mod" && $(2) ); \
done
endef

go.lint:
golangci-lint run $(GOLANGCI_LINT_FLAGS)
$(call run_in_modules,golangci-lint run,golangci-lint run --config $(GOLANGCI_LINT_CONFIG) $(GOLANGCI_LINT_FLAGS))

go.test:
go test $(GO_TEST_FLAGS) ./...
$(call run_in_modules,go test,go test $(GO_TEST_FLAGS) ./...)

go.test.unit:
go test -short $(GO_TEST_FLAGS) ./...
$(call run_in_modules,go test -short,go test -short $(GO_TEST_FLAGS) ./...)

go.build:
$(call run_in_modules,go build,go build ./...)

go.mod.update:
echo "==> update dependencies recursively"
go get -u ./...
echo "==> run 'go mod tidy'"
go mod tidy
$(call run_in_modules,go get -u + go mod tidy,go get -u ./... && go mod tidy)
echo "==> go work sync"
go work sync
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ You can add this library to your project by running:
go get -u github.com/get-eventually/go-eventually
```

## Examples

End-to-end examples live under [`examples/`](./examples):

* [`examples/todolist`](./examples/todolist) — a Connect-based TodoList
service demonstrating aggregates, commands, queries, and BDD-style
scenario tests, backed by the in-memory event store.

## Contributing

Thank you for your consideration ❤️ You can head over our [CONTRIBUTING](./CONTRIBUTING.md) page to get started.
Expand Down
96 changes: 96 additions & 0 deletions examples/todolist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# TodoList example

A small Connect-based service that exercises `go-eventually`'s DDD / Event
Sourcing primitives end-to-end. It serves as a real-world litmus test for
the library's API: if something feels awkward in this example, it probably
needs rethinking in the library.

## What this example demonstrates

- An aggregate root (`todolist.TodoList`) with a child entity
(`todolist.Item`), built on `aggregate.BaseRoot` and
`aggregate.RecordThat`.
- Commands (`todolist.CreateCommand`, `todolist.AddItemCommand`)
implementing `command.Handler[Cmd]`, persisted through an
`aggregate.EventSourcedRepository` backed by `event.NewInMemoryStore`.
- A query (`todolist.GetQuery`) implementing `query.Handler[Q, R]`,
reusing the same repository's `Get`.
- BDD-style test scenarios using `aggregate.Scenario`, `command.Scenario`,
and (implicitly through the command scenarios) the event streaming
plumbing.
- A Connect service exposing the above over HTTP/2 (h2c), with gRPC
health + gRPC reflection wired up.

Because the repository internally streams events through the new
`message.Stream[event.Persisted]` iterator, any request that triggers
`AddTodoItem` (which loads the existing aggregate) exercises the iterator
end-to-end.

## Running

```sh
go run ./examples/todolist
# or from the example dir:
cd examples/todolist && go run .
# Server listens on :8080 by default
```

The example is a member of the repository's Go workspace (`go.work` at
repo root). The workspace is what resolves the `go-eventually` import
to the in-repo library code. The example's `go.mod` still carries a
`require github.com/get-eventually/go-eventually v0.4.0` line as a
nominal floor — `GOWORK=off` would fall back to that released version,
which is NOT what you want when evaluating in-progress library changes.

Hit it with a Connect client, `grpcurl`, or the built-in reflection:

```sh
grpcurl -plaintext localhost:8080 list
grpcurl -plaintext -d '{"todo_list_id":"...","title":"chores","owner":"me"}' \
localhost:8080 todolist.v1.TodoListService/CreateTodoList
```

## Design choices worth noting

- **Commands return `google.protobuf.Empty`.** Clients generate IDs and
pass them in; the server acknowledges. Idempotent on retries with the
same ID.
- **Request/response messages colocated with the service** in
`todo_list_service.proto`; domain messages live in their own files.
Small service surface benefits more from colocation than from strict
one-message-per-file splitting.
- **Connect only.** No HTTP/REST gateway, no `google.api.http`
annotations. The Connect protocol itself already speaks gRPC, gRPC-Web,
and Connect-over-HTTP; that's enough surface for an example.
- **In-memory store.** State is lost on restart. The example is about
wiring, not persistence; swap `event.NewInMemoryStore()` in `main.go`
for a `postgres.NewEventStore(...)` to get durability.
- **Error handling is example-grade.** Domain errors are mapped to Connect
codes (`InvalidArgument`, `AlreadyExists`, `NotFound`, `Internal`) and
the full error chain is propagated to the client. A real service would
sanitize messages before they cross the wire.
- **Package by domain, not by layer.** Everything TodoList-related —
aggregate, events, commands, queries, handlers, Connect transport,
proto conversion — lives in a single `internal/todolist` package. Names
like `CreateCommand`, `GetQuery`, `AddItemCommandHandler`,
`ConnectServiceHandler`, and `ToProto` ride on top of the package
prefix to keep call sites terse and the domain boundary obvious at a
glance.

## Regenerating the protos

```sh
cd examples/todolist
buf generate
```

Committed output lives under `gen/`; regenerate after any proto change.
The buf configuration uses the v2 schema (see `buf.yaml` + `buf.gen.yaml`
at the module root).

## CI coverage

`make go.lint` and `make go.test` at the repo root iterate over every
Go workspace member (discovered from `go.work`). This example is
therefore lint-gated and test-gated on every PR — a library change
that breaks the example fails CI.
13 changes: 13 additions & 0 deletions examples/todolist/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/get-eventually/go-eventually/examples/todolist/gen
plugins:
- remote: buf.build/protocolbuffers/go:v1.31.0
out: gen
opt: paths=source_relative
- remote: buf.build/connectrpc/go:v1.12.0
out: gen
opt: paths=source_relative
19 changes: 19 additions & 0 deletions examples/todolist/buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: v2
modules:
- path: proto
lint:
use:
- COMMENTS
- PACKAGE_NO_IMPORT_CYCLE
- STANDARD
- UNARY_RPC
except:
- FIELD_NOT_REQUIRED
rpc_allow_google_protobuf_empty_responses: true
disallow_comment_ignores: true
breaking:
use:
- FILE
except:
- EXTENSION_NO_DELETE
- FIELD_SAME_DEFAULT
Loading
Loading