Go implementation of Two-Phase Commit (2PC) and Three-Phase Commit (3PC) protocols for distributed systems.
The coordinator initiates transactions and manages the commit protocol. Participants (called cohorts in the code and CLI) vote on each transaction and apply its outcome. Nodes communicate over gRPC and persist state using a database and write-ahead log (WAL).
Requires Docker with Compose:
git clone https://github.com/vadiminshakov/committer.git
cd committer
docker compose up --build --wait
docker compose exec coordinator committer put --addr 127.0.0.1:3000 greeting hello
docker compose exec coordinator committer get --addr 127.0.0.1:3000 greetingThe last command prints hello. Open the protocol visualization
and press Play. The coordinator's gRPC port is available at localhost:3000;
the participant is reachable only inside the Compose network. Both services have
health checks for their gRPC endpoints, and Compose waits for them to pass before
docker compose up returns. The sample write checks the full transaction path.
Ports 3000 and 8080 must be available.
Use docker compose logs -f to see node logs and docker compose down to stop
the nodes. Named volumes preserve their databases and WAL across restarts. To
delete the data, run docker compose down --volumes (this permanently removes
both nodes' data). This Compose setup is intended for local use.
Requires Go 1.25 or newer and make. Run commands from the repository root.
git clone https://github.com/vadiminshakov/committer.git
cd committer
make demoThis builds bin/committer, starts a coordinator and one participant (cohort)
using 2PC, writes greeting=hello, and reads it back:
Committed transaction 0
hello
Node: localhost:3000
Reachable: yes
Height: 1
The transaction number and height increase on subsequent runs. Open
the protocol visualization and press Play to see the
message exchange. Press Ctrl+C in the terminal to stop both demo nodes.
Ports 3000, 3001 and 8080 must be available. Logs are in .data/demo/logs/.
Demo data survives restarts in .data/demo/. To start from scratch, stop all
demo nodes, then run make demo-reset. This deletes only demo data.
make build
# Terminal 1: participant
./bin/committer cohort -nodeaddr localhost:3001 -coordinator localhost:3000
# Terminal 2: coordinator
./bin/committer coordinator -nodeaddr localhost:3000 -cohorts localhost:3001 -viz-port 8080
# Terminal 3: client
./bin/committer put --addr localhost:3000 greeting hello
./bin/committer get --addr localhost:3000 greeting
./bin/committer status --addr localhost:3000get prints hello. status reports reachability and the node's current height;
it is not a cluster health check. Put client flags before key/value arguments.
Quote values containing spaces: ./bin/committer put greeting "hello world".
Requests have a 5-second deadline, configurable with --timeout 10s.
put requires a coordinator. get reads the target node's local committed data.
If a request fails, check its error message and node logs. A timeout or lost
connection during put does not by itself establish whether the transaction committed.
For 3PC, pass -committype three-phase -timeout 1s to both nodes.
Run ./bin/committer --help or ./bin/committer coordinator -h for help.
The optional web UI animates protocol messages and shows an event log, transaction height, key and participants. Use Play/Pause, speed and replay controls to inspect the exchange. It is a protocol demonstration, not a metrics or health dashboard.
Enable it on a node with -viz-port 8080, then open http://localhost:8080.
Use a different port for each node's visualization. The HTTP server listens on
all interfaces; the displayed localhost URL is for local access.
The Two-Phase Commit protocol ensures atomicity in distributed transactions through two distinct phases:
- Coordinator sends a
PROPOSErequest to all cohorts with transaction data - Each Cohort validates the transaction locally and responds:
ACK(Yes) - if ready to commitNACK(No) - if unable to commit
- Coordinator waits for all responses
- If all cohorts voted
ACK:- Coordinator sends
COMMITto all cohorts - Each Cohort commits the transaction and responds with
ACK
- Coordinator sends
- If any cohort voted
NACK:- Coordinator sends
ABORTto all cohorts - Each Cohort aborts the transaction
- Coordinator sends
The Three-Phase Commit protocol extends 2PC with an additional phase to reduce blocking scenarios:
- Coordinator sends
PROPOSErequest to all cohorts - Cohorts respond with
ACK/NACK(same as 2PC)
- If all cohorts voted
ACK:- Coordinator sends
PRECOMMITto all cohorts - Cohorts acknowledge they're prepared to commit
- Timeout mechanism: If cohort doesn't receive
COMMITwithin timeout, it auto-commits
- Coordinator sends
- If any cohort voted
NACK:- Coordinator sends
ABORTto all cohorts
- Coordinator sends
- Coordinator sends
COMMITto all cohorts - Cohorts perform the actual commit operation
Node commands accept these flags:
| Flag | Description | Default |
|---|---|---|
nodeaddr |
Node listen address, host:port |
localhost:3050 |
coordinator |
Coordinator address; required by the cohort command |
empty |
cohorts |
Comma-separated participant addresses; required by coordinator |
empty |
committype |
two-phase or three-phase |
two-phase |
timeout |
3PC timeout, e.g. 1s or 500ms; bare numbers remain milliseconds |
1s |
data-dir |
Root for persistent databases and WAL | .data |
viz-port |
Protocol visualization HTTP port; 0 disables it | 0 |
Timeouts must be positive whole milliseconds. Client commands use their own
--timeout flag for the request deadline; it requires a duration such as 5s.
Node startup logs show the selected role, protocol, addresses and storage paths.
Normal starts never clear data. Databases and WAL live beneath
<data-dir>/db/<role>/<address>/ and <data-dir>/wal/<role>/<address>/.
Restart with the same working directory, data directory and address to reuse them.
Use an absolute -data-dir when launching from different working directories.
The original flag-only syntax remains supported: -cohorts selects the
coordinator role; otherwise the node is a cohort. Prefer explicit commands for
new scripts because their required and incompatible flags are validated.
With both nodes running:
go run ./examples/client -addr localhost:3000 -timeout 5sThe example writes and reads five keys (somekey0 through somekey4), printing
got value for key 'somekey0': somevalue0, and so on. Customize prefixes with
-key and -value. See the example source for bounded
requests, error handling and closing the client connection.
Custom validation and business logic for the Propose and Commit stages. Hooks run in registration order; returning false rejects the operation.
committer := commitalgo.NewCommitter(database, "three-phase", wal, timeout,
hooks.NewMetricsHook(),
hooks.NewValidationHook(100, 1024),
hooks.NewAuditHook("audit.log"),
)
// or register later
committer.RegisterHook(myCustomHook)PRs and issues are welcome.