These prohibitions and prescriptions apply to every inter-service call in the monorepo. They are explicit because in practice each one has been violated and caused a coupling or operability incident:
- Never use HTTP between services — Always use RabbitMQ RPC. Direct HTTP between
bin-*-managerservices bypasses circuit-breaker protection (see../patterns/circuit-breaker.md) and the per-target RPC metrics, and creates synchronous coupling that defeats the queue-buffering model. - Use typed request methods — Don't construct
sock.Requestmanually. Use therequesthandlertyped methods (e.g.,r.AgentV1AgentGet(...)); see §9.1 below. - Handle async responses — RabbitMQ RPC is asynchronous. Always pass
context.Contextand respect its deadline; don't assume responses are immediate. - Publish events for notifications — Use
notifyhandler.PublishEvent()for pub/sub notifications. Don't fan-out via N RPC calls; that's what the event broker is for.
All inter-service calls go through requesthandler.RequestHandler typed methods. Never call services directly:
// CORRECT — typed RPC call
agent, err := h.reqHandler.AgentV1AgentGet(ctx, agentID)
// WRONG — constructing raw RPC requests
req := &sock.Request{URI: "/v1/agents/" + id.String(), Method: "GET"}
resp, err := h.sockHandler.RequestPublish(ctx, "bin-manager.agent-manager.request", req)Incoming RPC requests are routed by regex matching on URI + method:
// CORRECT — regex routing pattern
var (
regV1Agents = regexp.MustCompile("/v1/agents$")
regV1AgentsGet = regexp.MustCompile(`/v1/agents\?(.*)$`)
regV1AgentsID = regexp.MustCompile("/v1/agents/" + regUUID + "$")
)
func (h *listenHandler) processRequest(m *sock.Request) (*sock.Response, error) {
ctx := context.Background() // fresh context per request
switch {
case regV1AgentsGet.MatchString(m.URI) && m.Method == sock.RequestMethodGet:
return h.processV1AgentsGet(ctx, m)
case regV1Agents.MatchString(m.URI) && m.Method == sock.RequestMethodPost:
return h.processV1AgentsPost(ctx, m)
}
}Services use three queues:
bin-manager.<service-name>.request # RPC requests
bin-manager.<service-name>.event # Published events
bin-manager.<service-name>.subscribe # Event subscriptions
bin-manager.delay # Shared delayed message queue
Use HTTP-style status codes in sock.Response:
// CORRECT
return &sock.Response{StatusCode: 200, DataType: "application/json", Data: data}, nil
return &sock.Response{StatusCode: 404}, nil
return &sock.Response{StatusCode: 500}, nilRequest structs in pkg/listenhandler/models/request/ MUST be flat — top-level fields directly, no nested Request wrapper. This matches calls.go, recordings.go, confbridge.go, etc.
// CORRECT — flat
type V1DataOutboundConfigsIDPut struct {
Name *string `json:"name,omitempty"`
Detail *string `json:"detail,omitempty"`
DestinationWhitelist *[]string `json:"destination_whitelist,omitempty"`
Codecs *string `json:"codecs,omitempty"`
}
// WRONG — wrapper introduces a wire-format mismatch with bin-common-handler clients
type V1DataOutboundConfigsIDPut struct {
Request outboundconfig.UpdateRequest `json:"request"`
}Why this matters. Wire-format wrappers must agree on both sides — bin-common-handler/pkg/requesthandler/*.go marshals the body, bin-call-manager/pkg/listenhandler/v1_*.go unmarshals it. json.Unmarshal silently ignores unknown top-level keys, so a mismatch produces a zero-valued struct with no error. The downstream handler then operates on nil/zero fields and the bug surfaces as "200 OK but nothing changed." A real production incident — see ../workflows/common-gotchas.md (Listener Wire-Format Mismatch).
Domain models stay where they belong. A handler-layer "partial update" model with pointer fields (e.g., outboundconfig.UpdateRequest) is legitimately reused by the DB layer to build dynamic UPDATE … SET SQL. Keep it in models/<entity>/. The listener translates the flat RPC struct into the domain model:
var req request.V1DataOutboundConfigsIDPut
if err := json.Unmarshal(m.Data, &req); err != nil { ... }
updateReq := &outboundconfig.UpdateRequest{
Name: req.Name,
Detail: req.Detail,
DestinationWhitelist: req.DestinationWhitelist,
Codecs: req.Codecs,
}
c, err := h.outboundConfigHandler.Update(ctx, id, updateReq)Every new requesthandler typed method + listener handler pair must:
- Listener model is flat. No
Requestwrapper field; no import of the domain model frompkg/listenhandler/models/request/. (§9.5) - Client marshals via the listener model.
json.Marshal(cmrequest.V1DataXxx{...})— neverjson.Marshal(domainRequest)or an inline anonymous struct. - Listener test asserts the parsed payload. Replace
gomock.Any()with a field-by-field matcher on the request body. (Seetesting.md§13.8.) - Client test asserts the marshaled wire shape. Mock
sockHandler.RequestPublishand verifysock.Request.Databyte-for-byte. (Seetesting.md§13.9.)
If any one of these is missing, a wire-format mismatch can silently round-trip as a no-op. All four must be in place.