Omnichannel Engagement: Real-Time Presence and Routing in Go
A customer support gateway built to prove a different Go skill than my microservices work: WebSocket concurrency, Redis-backed presence with a heartbeat instead of a boolean, and an SLA scheduler, all verified against a real running stack rather than assumed from the code.
Omnichannel Engagement: Real-Time Presence and Routing in Go
I already have a Go project in my portfolio that proves microservices decomposition: a logistics platform with 9 services talking over gRPC and NATS. Building another service-heavy Go project would prove the same thing twice. Omnichannel Engagement is deliberately a different problem: one process, a lot of long-lived WebSocket connections, and state that has to stay correct across connections coming and going in real time.
The Shape of the Problem
Customers and agents both connect over WebSocket to the same Hub. Every new conversation needs to go to whichever online agent currently has the fewest active conversations. Presence (who's online right now) has to be answerable even though a WebSocket connection is pinned to whatever process accepted it. And if an agent goes quiet on an assigned conversation, something has to notice without any human action triggering it.
The Assignment Decision Is Pure Go, Nothing Else
func PickLeastBusy(loads []domain.AgentLoad) (domain.Agent, error) {
var best *domain.AgentLoad
for i := range loads {
if loads[i].Agent.Status != domain.AgentOnline {
continue
}
if best == nil || loads[i].ActiveCount < best.ActiveCount {
best = &loads[i]
}
}
if best == nil {
return domain.Agent{}, ErrNoAgentAvailable
}
return best.Agent, nil
}
No database, no Redis, no network in this function. It takes a slice of agents-with-their-current-load and returns a decision. That means the routing logic is tested with hand-built structs in milliseconds, and the Hub's job is reduced to fetching the load data and acting on the decision, not making the decision itself.
Presence Is a Redis Key With a Heartbeat, Not a Boolean
The tempting shortcut is an in-memory map[string]bool of online agents. That works exactly until there's a second gateway instance, at which point it's simply wrong: an agent connected to instance A is invisible to instance B. Presence here is a Redis key (presence:agent:) with a TTL, refreshed by a heartbeat while the connection is alive, plus a pub/sub channel so other instances hear about changes in real time instead of polling. The TTL matters as much as the key: if a gateway instance crashes without a clean disconnect, its agents' keys simply expire instead of showing "online" forever.
Proving It, Not Assuming It
Unit tests cover the pure logic, but the interesting behavior here is inherently about real connections, real timing, and a real external store, so it was checked against the real stack with a small Go WebSocket test client rather than trusting the code:
$ go run ./cmd/testclient -role agent -id agent-1 -listen 15 # 2 active conversations
$ go run ./cmd/testclient -role agent -id agent-2 -listen 15 # 0 active conversations
$ go run ./cmd/testclient -role customer -id cust-3 -msg "New customer here" -listen 6
[customer cust-3] received:
{ "type": "assigned", "agent_id": "agent-2", "agent_name": "Bagas" }
Correctly skipped the busier agent. Presence was checked the same way, reading the actual Redis key while connected and confirming it disappeared after disconnect:
$ redis-cli GET presence:agent:agent-2
"ONLINE"
$ redis-cli TTL presence:agent:agent-2
(integer) 28
<h1>...agent disconnects...</h1>
$ redis-cli KEYS "presence:*"
(empty array)
And the SLA scheduler was checked by shortening its window to a few seconds and watching a real escalation event arrive over an open connection, not by asserting the internal timer logic in isolation.
An Honest Contrast With Payment Guard
I built this right after Payment Guard (Java/Spring Boot), and the difference in where bugs showed up is worth naming honestly rather than glossing over. Payment Guard's most interesting bug (a ledger foreign-key ordering issue) only appeared once the system actually ran against a real Postgres, because Hibernate and Spring's reflection-heavy stack defer a lot of correctness to runtime. Here, go build and go vet caught every structural mistake, including a type mismatch between an agent ID and a conversation's agent reference, before the program ran even once. Go's simpler, more static toolchain moved that entire class of bug earlier. It did not, however, replace the need to actually run the least-busy routing, the presence TTL, and the SLA escalation against a real Redis, a real Postgres, and real WebSocket connections above: none of that is a compile-time property in any language, Go included.
What's Not Here
No frontend widget: this project's story is the concurrency and protocol layer, not a chat UI. No automated tests for the Hub, presence, or SLA scheduler themselves, they're integration-shaped (real connections, real timing) rather than unit-shaped, and were verified manually instead. And only one gateway instance was ever actually run: the presence design is ready for more than one (Redis, not memory, is the source of truth), but a second instance actually receiving another instance's presence events was never observed firsthand, so I'm not claiming it as proven.
Lessons Learned
Keep the decision pure, keep the plumbing separate. PickLeastBusy taking a plain slice and returning a plain value is what makes it trivial to test and easy to trust.
A TTL is a design decision, not an implementation detail. The difference between "presence is a boolean" and "presence is a heartbeat-refreshed key with a TTL" is the difference between a system that lies about a crashed instance and one that self-corrects.
Compile-time safety and runtime verification answer different questions. Go's type system caught mistakes Java's runtime stack would have let through until execution. Neither one tells you your SLA scheduler actually fires on time against a real clock and a real database, that still has to be watched happening.
Comments
No comments yet. Be the first!