ThreeDotsLabs/watermill: a Go library for event-driven applications without the broker lock-in
Building event-driven applications the easy way in Go.
At a glance
- What is it?
- Watermill wraps Kafka, RabbitMQ, NATS, SQL and HTTP behind one Publisher/Subscriber interface and a message router. It suits Go teams that want event-driven structure without adopting a framework.
- Who is it for?
- Adopt watermill if you are writing Go services that need to publish and consume messages and you want the transport to stay swappable. Do not adopt it if you expect a framework to give you a broker, a schema registry or a deployment topology; watermill is a library and the operational side stays yours.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 36 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem watermill addresses in a Go service
Most Go codebases reach the point where a request has to trigger work that should not run inside the request. The usual first move is a goroutine, then a channel, then a queue table, and eventually a broker client library with its own types, its own error handling and its own retry story. Each of those steps couples the application to a specific transport. Watermill targets that coupling. The README states the goal plainly: make communication with messages as easy to use as HTTP routers. The audience is Go developers building event-driven architecture, CQRS, sagas, RPC over messages or stream processing, and who want to start before they have studied the whole discipline. The README is explicit that you should not need to know the whole stack to begin, the same way you do not need the TCP stack to write an HTTP server.
One handler signature and a router in the middle
The core of the library is a single function shape. The README gives it as func(*Message) ([]*Message, error). A handler receives a message and either publishes new messages or returns an error, and middlewares decide what happens next. Around that sits the router, which is what most applications actually wire up. Transports are hidden behind two interfaces. Publisher exposes Publish(topic string, messages ...*Message) error and Close() error. Subscriber exposes Subscribe(ctx context.Context, topic string) (<-chan *Message, error) and Close() error. Because both are interfaces, the transport is a constructor argument rather than a compile-time dependency of your business logic. The repository ships the implementations as separate Go modules, which is why the import paths in the README carry versions such as github.com/ThreeDotsLabs/watermill-kafka/v3 and github.com/ThreeDotsLabs/watermill-sql/v4. The core module in go.mod pulls in uuid, ulid, shortuuid, protobuf, Prometheus client and gobreaker, which tells you what the library itself considers in scope: message identity, serialization, metrics and circuit breaking. The supported transports listed in the README are AMQP (RabbitMQ), AWS SNS/SQS, Bolt, Firestore, Google Cloud Pub/Sub, HTTP, io.Reader/io.Writer, Kafka, NATS Jetstream, Redis Streams, SQL for MySQL and PostgreSQL, and a SQLite implementation marked Beta. The README also points to an Awesome Watermill list for unofficial integrations.
Installing watermill and publishing a first message
The README points readers at the Quickstart and Getting Started guide on watermill.io and at the examples directory, with _examples/basic/1-your-first-app marked as the starting point. There is no install command in the README beyond adding the module, and the go.mod file shows the module path. Add the core module and one transport module to your project. The version suffixes matter: the Kafka module is imported at /v3 and the SQL module at /v4, so copying an older import path from a blog post will not resolve.
go get github.com/ThreeDotsLabs/watermill
go get github.com/ThreeDotsLabs/watermill-kafka/v3The next step is to construct a router, which the documentation describes as the object that connects subscribers to handlers. The README does not print a full router example, so the canonical code lives in the quickstart and in _examples/basic/3-router. What the README does give is the shape every handler must satisfy, and this is the signature to keep in mind while reading the examples:
func(*Message) ([]*Message, error)A handler returning a non-nil error hands the message to whatever middleware you attached, and the README states that what happens next is up to those middlewares. That is the whole control flow. Before writing production code, run the repository's own checks to confirm your Go toolchain matches the module, which requires Go 1.25.0 according to go.mod.
go test ./... -shortThe Makefile defines that target as test_short, and it is the fastest of the test targets listed there. The full suite is make test, and there are separate targets for race detection, stress tests behind a build tag and reconnect tests, which is a useful signal about where the maintainers expect flakiness to live.
Where watermill stops and you start
Watermill is a library, not a platform, and the README does not pretend otherwise. It will not run a broker for you, manage consumer group rebalancing for you, or give you a schema registry. Delivery semantics are the sharpest edge. The README's training description lists handling at-least-once delivery as a concept to learn, and the examples include an exactly-once delivery counter under _examples/real-world-examples. That framing is honest and worth reading carefully: exactly-once is presented as a pattern you implement with a specific transport and a specific storage model, not a flag you set on the router. If your application cannot tolerate a duplicate message without an idempotency key, watermill will not save you from designing one. The second limitation is fragmentation across modules. Because each transport is its own repository and module with its own major version, upgrading watermill core does not upgrade your Kafka client behaviour, and the compatibility matrix is something you assemble yourself. The third is that the README's stability note covers the public API of the core library; it says nothing about the API surface of the individual Pub/Sub modules, which carry their own version numbers precisely because they move independently.
Watermill compared with writing directly against a broker client
The alternative most Go teams actually weigh is not another framework but the broker's own client library, or a higher-level toolkit such as the CloudEvents SDK plus a raw consumer. The difference is where the abstraction sits. A raw Kafka client gives you partitions, offsets, rebalance callbacks and consumer groups as first-class concepts, and your application code is written in those terms. Watermill inverts that: your handler sees a Message and returns messages or an error, and the transport is a constructor you can replace. The practical consequence is that a handler written against the AMQP Pub/Sub can be pointed at Kafka or Redis Streams with a different constructor, which is useful for local development and for tests. The cost is that transport-specific features have to be reached through configuration of the Pub/Sub rather than through your handler, and anything the interface does not model, such as partition assignment, is either exposed by that module or not available at all. If your design depends on fine-grained control over partition assignment or on broker-specific transactions, the raw client is the more direct tool. Watermill earns its place when the message flow is the interesting part and the broker is an implementation detail.
Maintenance, licensing and upgrade cost
The repository is not archived, and the last push was on 2026-08-25, the same date as the v1.5.3 release. The release history shows v1.5.2 on 2026-05-13 and v1.5.1 on 2025-09-02, so patches arrive when there is something to patch rather than on a schedule. The README states that v1.0.0 is production-ready and that the public API is stable and will not change without a major version bump, which is the strongest maintenance signal available here. The upgrade path is documented in the repository root: UPGRADE-0.3.md, UPGRADE-0.4.md and UPGRADE-1.0.md exist as separate files, and RELEASE-PROCEDURE.md describes how releases are cut. For a team on v1.x, the realistic upgrade cost is not the core library but the transport modules, each of which versions independently. The licence is MIT, which permits commercial use and modification; the repository contains a single LICENSE file and no separate licensing terms are mentioned for the core module. The individual Pub/Sub modules live in their own repositories, so confirm the licence of the one you depend on rather than assuming it matches. Nothing here is legal advice, and the MIT text in the repository is the authoritative statement.
Editorial conclusion
Adopt watermill if you are writing Go services that need to publish and consume messages and you want the transport to stay swappable. Do not adopt it if you expect a framework to give you a broker, a schema registry or a deployment topology; watermill is a library and the operational side stays yours. Before committing, read the source of the Pub/Sub you plan to use and check how it handles offsets or acknowledgements, because at-least-once delivery is the behaviour the documentation describes and exactly-once is presented as something you build with an example rather than something you configure.
Frequently asked questions
How do I use watermill in a Go project?
Add the core module and one transport module with go get, then construct a router that connects a Subscriber to a handler. The handler must match func(*Message) ([]*Message, error), and the README points to the quickstart and to _examples/basic/1-your-first-app as the place to start.
What is ThreeDotsLabs/watermill used for?
The README describes it as a Go library for working efficiently with message streams, intended for event-driven applications, event sourcing, RPC over messages and sagas. It works with conventional pub/sub systems such as Kafka or RabbitMQ, and also with HTTP or PostgreSQL.
How does watermill work internally?
Publishers and subscribers implement two interfaces, one for publishing to a topic and one for subscribing to a topic and returning a channel of messages. A router connects them, and middlewares decide what happens when a handler returns an error or publishes new messages.
What does a watermill do?
In this library, the router receives messages from a subscriber, passes them to a handler matching func(*Message) ([]*Message, error), and publishes whatever the handler returns. The README describes the goal as making communication with messages as easy to use as HTTP routers.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/threedotslabs-watermill)