Building a Microservice
This guide walks through building a microservice that exercises the 6 user-facing features of the Connector - functional endpoints, web handlers, outbound events, inbound event sinks, configuration properties, and tickers. The other 2 features (workflow tasks and workflow definitions) are covered in Building Agentic Workflows.
The example throughout is an inventory microservice that exposes its stock to other services, fires events when stock runs low, listens for order events to decrement stock, and refreshes from a warehouse system on a schedule.
Step 1: Create the Microservice
Start by asking the agent to scaffold a new microservice:
The agent uses a skill to lay down the directory structure, generate the boilerplate, and register the microservice in main/main.go. See My First Microservice for what the scaffolded layout looks like.
Step 2: Add a Functional Endpoint
Functional endpoints are typed RPC functions other microservices call. They are the primary surface for service-to-service communication.
Each functional endpoint receives ctx context.Context followed by typed input arguments and returns typed output arguments plus an err. The framework marshals inputs from the path, query string, or JSON body, and marshals outputs as JSON.
func (svc *Service) GetStock(ctx context.Context, sku string) (count int, err error) {
svc.stockMu.RLock()
defer svc.stockMu.RUnlock()
return svc.stock[sku], nil
}The route defaults to the function name in kebab-case and may include path arguments:
# manifest.yaml Representation
functions:
GetStock:
signature: GetStock(sku string) (count int)
description: GetStock returns the current count for a SKU.
method: GET
route: /stock/{sku}Upstream microservices reach the endpoint through a generated client stub:
count, err := inventoryapi.NewClient(svc).GetStock(ctx, "WIDGET-1")The endpoint can also be exercised in isolation through the generated Executor in the API package:
t.Run("known_sku", func(t *testing.T) {
assert := testarossa.For(t)
count, err := exec.GetStock(ctx, "WIDGET-1")
assert.Expect(count, 42, err, nil)
})Step 3: Add a Web Handler
Web handlers give the microservice direct access to the http.ResponseWriter for HTML pages, file downloads, or any response that needs full control of the HTTP envelope.
# manifest.yaml Representation
webs:
Dashboard:
signature: Dashboard()
description: Dashboard renders the inventory dashboard page.
method: GET
route: //dashboardfunc (svc *Service) Dashboard(w http.ResponseWriter, r *http.Request) (err error) {
svc.stockMu.RLock()
rows := make([]item, 0, len(svc.stock))
for sku, count := range svc.stock {
rows = append(rows, item{SKU: sku, Count: count})
}
svc.stockMu.RUnlock()
return svc.WriteResTemplate(w, "dashboard.html", rows)
}Web handlers can take path arguments and have full access to request headers, query parameters, body and form data through r.PathValue, r.URL.Query(), etc.
Step 4: Fire Outbound Events
Outbound events are fire-and-forget messages a microservice broadcasts to notify others of something that happened, without knowing who (if anyone) is listening.
# manifest.yaml Representation
outboundEvents:
OnStockLow:
signature: OnStockLow(sku string, count int)
description: OnStockLow fires when stock for a SKU drops below threshold.
method: POST
route: :417/on-stock-lowThe microservice fires the event using the generated multicast trigger. Outbound events use port :417 by default to differentiate them from regular endpoints on :443.
func (svc *Service) decrementStock(ctx context.Context, sku string, n int) error {
svc.stockMu.Lock()
svc.stock[sku] -= n
count := svc.stock[sku]
svc.stockMu.Unlock()
if count < svc.LowStockThreshold() {
for range inventoryapi.NewMulticastTrigger(svc).OnStockLow(ctx, sku, count) {
}
}
return nil
}The for range loop iterates over subscribers’ acknowledgements - useful when the producer wants to know which subscribers received the event. Producers that don’t care about subscribers can ignore the loop body.
Step 5: Subscribe to Inbound Events
Inbound event sinks are the other side of outbound events: a sink subscribes to events emitted by another microservice. The sink’s signature must match the source event.
# manifest.yaml Representation
inboundEvents:
OnOrderPlaced:
signature: OnOrderPlaced(orderID string, items []LineItem)
description: OnOrderPlaced decrements stock for each line item in the order.
source: github.com/example/ordersfunc (svc *Service) OnOrderPlaced(ctx context.Context, orderID string, items []LineItem) (err error) {
for _, item := range items {
if err := svc.decrementStock(ctx, item.SKU, item.Quantity); err != nil {
svc.LogError(ctx, "decrement failed", "sku", item.SKU, "err", err)
}
}
return nil
}If two source microservices emit events with the same name, the sink can rename its handler to disambiguate:
inboundEvents:
OnOrderPlacedRetail:
signature: OnOrderPlacedRetail(orderID string, items []LineItem)
event: OnOrderPlaced
source: github.com/example/retail
OnOrderPlacedWholesale:
signature: OnOrderPlacedWholesale(orderID string, items []LineItem)
event: OnOrderPlaced
source: github.com/example/wholesaleStep 6: Add Configuration Properties
Configuration properties are runtime settings the microservice declares for itself. Their values are managed externally and delivered by the configurator core microservice.
# manifest.yaml Representation
configs:
LowStockThreshold:
description: Stock count at or below this triggers OnStockLow.
type: int
validation: int [1,1000]
default: 10// Generated accessor, used directly by handler code.
threshold := svc.LowStockThreshold()
// Optional change callback, invoked when the value is updated at runtime.
func (svc *Service) OnChangedLowStockThreshold(ctx context.Context) (err error) {
svc.LogInfo(ctx, "low-stock threshold updated", "value", svc.LowStockThreshold())
return nil
}Values are set per-microservice in config.yaml using the microservice’s hostname as the key:
inventory.example:
LowStockThreshold: 25Supported types are string, bool, int, float64 and time.Duration. Validation rules are enforced by the configurator before a new value is accepted.
Step 7: Add a Ticker
Tickers trigger a recurring operation on a periodic basis - useful for polling, cleanup jobs, cache refreshes, and other scheduled work.
# manifest.yaml Representation
tickers:
RefreshFromWarehouse:
description: RefreshFromWarehouse pulls a fresh stock snapshot.
interval: 15mfunc (svc *Service) RefreshFromWarehouse(ctx context.Context) (err error) {
resp, err := svc.http.Get("https://warehouse.example.com/stock.json")
if err != nil {
return errors.Trace(err)
}
defer resp.Body.Close()
var snapshot map[string]int
if err := json.NewDecoder(resp.Body).Decode(&snapshot); err != nil {
return errors.Trace(err)
}
svc.stockMu.Lock()
svc.stock = snapshot
svc.stockMu.Unlock()
return nil
}The ctx passed to the ticker is canceled when the microservice shuts down, so a long-running iteration terminates gracefully on a graceful shutdown signal.
Run and Test
Run the application from the project’s main/ directory:
cd main
go run main.goThe microservice is reachable through the HTTP ingress:
GET http://localhost:8080/inventory.example/stock/{sku}- the functional endpointGET http://localhost:8080/inventory.example/dashboard- the web handler
The ticker fires every 15 minutes. Outbound events on OnStockLow are delivered to any subscriber on the bus.
For full end-to-end verification, the integration test harness spins up the inventory microservice along with mocked downstream dependencies in a single Go test:
func TestInventory_DecrementsOnOrder(t *testing.T) {
assert := testarossa.For(t)
// Spin up inventory plus a mock orders microservice in one process.
InitInventory()
orders := InitOrdersMock()
// Fire OnOrderPlaced from the mock and assert the stock dropped.
err := orders.PublishOnOrderPlaced(ctx, "ORDER-1", []LineItem{
{SKU: "WIDGET-1", Quantity: 3},
})
assert.NoError(err)
count, err := inventoryapi.NewClient(orders).GetStock(ctx, "WIDGET-1")
assert.Expect(count, 39, err, nil)
}Further Reading
- Connector - reference catalog of all 8 microservice features
- Building Agentic Workflows - the other 2 features (tasks and workflow definitions)
- Path arguments - wildcard and greedy path parameters
- HTTP arguments - fine-grained control over the HTTP request and response
- Configuration - the configurator microservice and config delivery
- Tickers - scheduled job mechanics
- Authorization - guarding endpoints with
requiredClaims - Integration testing - the in-process test harness