calculator

The calculator.example microservice performs simple integer arithmetic over the bus. It is the canonical demonstration of three Microbus capabilities:

  • Functional endpoints — handlers that look like normal Go functions; the framework parses query arguments (typically JSON over HTTP) and dispatches the call.
  • Custom struct types in endpoint signatures, declared in the API package so they form part of the microservice’s public contract.
  • Observable metrics — a counter incremented inline plus a gauge measured just-in-time when the metrics endpoint is scraped.

The service has no persistence and no downstream dependencies. The hello example uses calculator’s generated client to demonstrate service-to-service calls.

Try It

With the examples app running:

If an argument cannot be parsed, the framework returns an error:

http://localhost:8080/calculator.example/square?x=not-valid
→ json: cannot unmarshal string into Go struct field .x of type int

Code Walkthrough

Source: exampleservices/calculator/service.go.

The Service struct embeds the generated *Intermediate and adds four atomic accumulators that back the SumOperations gauge metric:

type Service struct {
    *Intermediate // IMPORTANT: Do not remove

    sumAdd      atomic.Int64
    sumSubtract atomic.Int64
    sumMultiply atomic.Int64
    sumDivide   atomic.Int64
}

atomic.Int64 is used instead of a mutex-guarded field because the accumulation is a single add with no read-modify-write dependency. OnStartup and OnShutdown are empty — calculator needs no lifecycle wiring.

Wiring (intermediate.go)

service.go only holds the hand-written handler bodies. The actual registration of endpoints and metrics with the underlying connector happens in the generated intermediate.go — produced by the code generator alongside the catalog file manifest.yaml. The constructor is the one entry point a calling app touches:

// NewService creates a new instance of the microservice.
func NewService() *Service {
    svc := &Service{}
    svc.Intermediate = NewIntermediate(svc)
    return svc
}

NewIntermediate does the wiring. The interesting calls for this example are the three endpoint subscriptions and the two metric declarations:

svc.Subscribe(
    "Arithmetic", svc.doArithmetic,
    sub.At(calculatorapi.Arithmetic.Method, calculatorapi.Arithmetic.Route),
    sub.Description(`Arithmetic performs an arithmetic operation between two integers x and y given an operator op.`),
    sub.Function(calculatorapi.ArithmeticIn{}, calculatorapi.ArithmeticOut{}),
)
// ... Square and Distance subscribed the same way ...

svc.DescribeCounter("used_operators", "UsedOperators tracks the types of the arithmetic operators used.")
svc.DescribeGauge("sum_operations", "SumOperations tracks the total sum of the results of all operators.")

Subscribe binds an HTTP method + route to a handler and supplies the input/output types the framework uses to parse and serialize the wire format. DescribeCounter and DescribeGauge register the metrics with the connector; the generated IncrementUsedOperators and RecordSumOperations helpers called from service.go resolve to these names.

This is also where the lifecycle hooks (SetOnStartup, SetOnShutdown, SetOnObserveMetrics, …) are bound back to the methods on *Service, which is how OnObserveSumOperations gets called on each metrics scrape.

Arithmetic

func (svc *Service) Arithmetic(ctx context.Context, x int, op string, y int) (xEcho int, opEcho string, yEcho int, result int, err error) {
    if op == " " {
        op = "+" // + is interpreted as a space in URLs
    }
    switch op {
    case "+":
        result = x + y
        svc.sumAdd.Add(int64(result))
    case "-":
        result = x - y
        svc.sumSubtract.Add(int64(result))
    case "*":
        result = x * y
        svc.sumMultiply.Add(int64(result))
    case "/":
        result = x / y // Intentional: division by zero panics; the platform recovers it as a 500 error
        svc.sumDivide.Add(int64(result))
    default:
        return x, op, y, result, errors.New("invalid operator '%s'", op, http.StatusBadRequest)
    }
    svc.IncrementUsedOperators(ctx, 1, op)
    return x, op, y, result, nil
}

The handler signature is a plain Go function — the framework binds x, op, and y from query arguments and serializes xEcho, opEcho, yEcho, result as a JSON response. No request/response objects to declare.

Two things worth calling out:

  • + becomes " " in URLs. The leading if op == " " normalizes it back so the echoed opEcho matches what the caller intended. Without this, a URL like ?op=+ would arrive as op=" " and fall through to the default branch as an “invalid operator.”
  • Division by zero is intentionally left to panic. The platform’s panic recovery converts it to a 500 error. This is a deliberate demonstration of how the framework handles unexpected runtime errors. For expected validation failures the handler attaches a status code explicitly — note errors.New("invalid operator '%s'", op, http.StatusBadRequest) in the default branch. Without the explicit http.StatusBadRequest, the framework’s default for an unannotated error is http.StatusInternalServerError (500), which would mis-classify a user typo as a server fault.

After each operation, two metric updates happen inline: the operator-keyed atomic.Int64 accumulator is bumped (for the SumOperations gauge), and the UsedOperators counter is incremented via the generated IncrementUsedOperators helper.

Square

func (svc *Service) Square(ctx context.Context, x int) (xEcho int, result int, err error) {
    svc.IncrementUsedOperators(ctx, 1, "^2")
    return x, x * x, nil
}

A one-argument variant. The UsedOperators counter is incremented with the label "^2" so the metric distinguishes squares from the four arithmetic operators.

Distance

func (svc *Service) Distance(ctx context.Context, p1 calculatorapi.Point, p2 calculatorapi.Point) (d float64, err error) {
    dx := p1.X - p2.X
    dy := p1.Y - p2.Y
    return math.Sqrt(dx*dx + dy*dy), nil
}

Distance exists to demonstrate custom struct types in endpoint signatures. Point is declared in the calculatorapi package so it ships as part of the microservice’s public API:

// Point is a 2D coordinate (X, Y)
type Point struct {
    X float64 `json:"x,omitzero"`
    Y float64 `json:"y,omitzero"`
}

An endpoint that accepts complex types can be called with a JSON request body — but Distance is registered with method ANY so it can also be exercised straight from a browser address bar. For that path, the framework deserializes nested struct fields from dot-notated query arguments, so p1.x=0&p1.y=0&p2.x=3&p2.y=4 lands as two fully-populated Point values without any custom binding code.

OnObserveSumOperations

func (svc *Service) OnObserveSumOperations(ctx context.Context) (err error) {
    svc.RecordSumOperations(ctx, int(svc.sumAdd.Load()), "+")
    svc.RecordSumOperations(ctx, int(svc.sumSubtract.Load()), "-")
    svc.RecordSumOperations(ctx, int(svc.sumMultiply.Load()), "*")
    svc.RecordSumOperations(ctx, int(svc.sumDivide.Load()), "/")
    return nil
}

SumOperations is declared as an observable gauge by the svc.DescribeGauge("sum_operations", ...) call wired in intermediate.go above. Observable gauges are not pushed on every change; instead, the framework calls OnObserveSumOperations on the metrics-scrape interval and asks it to report the current value.

In this example, the running totals are maintained inline in Arithmetic simply because that was the most convenient place to accumulate them — the observer just loads each atomic.Int64 and reports it. There is nothing in the framework that requires the observer itself to be cheap; it could equally compute its values on demand if that fit the data better.

See Also

  • Functional endpoints — how the framework binds query arguments and serializes responses.
  • Metrics — counter vs observable gauge, and the JIT observer pattern.
  • calculatorapi — the generated client package; see how hello uses it for service-to-service calls.