integration
embedding phalanx in your own application or using it as a service.
There are two ways to use Phalanx: as a standalone service you deploy and talk to over gRPC, or as a Go library you embed directly into your application.
option 1 — use as a service
Deploy a Phalanx cluster and interact with it via gRPC from any language. This is the simplest approach and works today.
from go
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/encoding"
)
// Register the JSON codec (once, at init)
encoding.RegisterCodec(jsonCodec{})
// Connect to the cluster leader
conn, _ := grpc.Dial("leader:9000",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(grpc.CallContentSubtype("json")),
)
// Write
req := map[string]any{"data": encodeCommand("SET", "foo", "bar")}
resp := map[string]any{}
conn.Invoke(ctx, "/phalanx.KV/Propose", req, &resp)
// Read
readReq := map[string]any{"key": "foo"}
readResp := map[string]any{}
conn.Invoke(ctx, "/phalanx.KV/Read", readReq, &readResp)from python
import grpc
import json
# Custom JSON codec channel
channel = grpc.insecure_channel('leader:9000')
# Use generic unary call with JSON serialization
method = '/phalanx.KV/Propose'
request = json.dumps({
"data": base64.b64encode(json.dumps({
"op": "SET", "key": "foo", "value": "bar"
}).encode()).decode()
}).encode()
response = channel.unary_unary(method)(request)from any http client (via grpc-web proxy)
If you front Phalanx with a gRPC-Web proxy like Envoy or grpcwebproxy, any HTTP client can interact with the KV service using JSON payloads.
option 2 — embed as a go library
Import Phalanx packages directly into your Go application. This gives you full control over the consensus engine — you can build distributed locks, replicated queues, or coordination services.
import the raft core
The raft/ package is a pure state machine with zero I/O dependencies. You drive it with Tick() and Step() calls:
import (
"phalanx/raft"
"phalanx/pb"
)
r := raft.NewRaft(raft.Config{
ID: "my-node",
Peers: []string{"peer-1", "peer-2"},
ElectionTimeout: 10,
HeartbeatTimeout: 3,
Logger: slog.Default(),
Term: &atomic.Uint64{},
})
// Drive the state machine
r.Tick() // advance logical clock
r.Step(incomingMessage) // process a message
msgs := r.Messages() // get outbound messages
entries := r.ApplicableEntries() // get committed entriesuse the full node
import phalanx "phalanx"
node, _ := phalanx.NewNode(phalanx.NodeConfig{
ID: "my-node",
Peers: []string{"peer-1", "peer-2"},
TickInterval: 100 * time.Millisecond,
ElectionTimeout: 10,
HeartbeatTimeout: 3,
DataDir: "/data/my-app",
GRPCAddr: "[::]:9000",
DebugAddr: "[::]:8080",
Logger: myLogger,
Term: &atomic.Uint64{},
})
// Wire peer addresses
node.SetPeerAddr("peer-1", "10.0.0.2:9000")
node.SetPeerAddr("peer-2", "10.0.0.3:9000")
// Run the event loop (blocks until ctx is cancelled)
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
node.Run(ctx)build a custom state machine
The current KV FSM (fsm/kv.go) implements SET and DELETE. To build your own state machine, follow the same pattern:
// Your custom state machine
type DistributedQueue struct {
mu sync.RWMutex
items []string
}
func (q *DistributedQueue) Apply(data []byte) error {
var cmd QueueCommand
json.Unmarshal(data, &cmd)
q.mu.Lock()
defer q.mu.Unlock()
switch cmd.Op {
case "PUSH":
q.items = append(q.items, cmd.Value)
case "POP":
if len(q.items) > 0 {
q.items = q.items[1:]
}
}
return nil
}to fully replace the KV FSM with your own, you would modify
node.goto accept aStateMachineinterface instead of the hardcodedfsm.KV. this is a planned refactor — see the architecture notes on component boundaries.
package reference
| package | import path | what you get |
|---|---|---|
raft/ | phalanx/raft | pure consensus state machine — Tick, Step, Propose, HasLeaderQuorum |
fsm/ | phalanx/fsm | KV state machine — Apply, Get, Snapshot |
storage/ | phalanx/storage | BadgerDB persistence — SaveState, LoadState, AppendLog |
network/ | phalanx/network | gRPC transport — send/receive RPCs, KV channels |
discovery/ | phalanx/discovery | SWIM gossip — automatic peer discovery, join/leave events |
pb/ | phalanx/pb | RPC types — LogEntry, request/response structs, service interfaces |
observability/ | phalanx/observability | metrics + debug HTTP handler |
logger/ | phalanx/logger | structured slog with atomic term injection |
design constraints for embedders
- the Raft state machine is not thread-safe. all calls to
Tick(),Step(),Propose(), andMessages()must happen from a single goroutine (the event loop). Messages()drains the outbound buffer. call it once per event cycle, not multiple times.ApplicableEntries()advanceslastApplied. call it once, apply all returned entries to your FSM in order, and do not retry.- peer addresses must be registered via
SetPeerAddr()beforeRun()unless you're using gossip discovery.