Broker-Based Math Service
Route addition and subtraction requests through typed broker channels to specialized services.
Examplemathbrokerrequest-responseexpressionsmicroservicesuiapirest
π Horizon
Math Service at a Glance
Overview
The system accepts addition and subtraction requests. A coordinating service delegates each operation to a specialized service and returns the result.
Architecture
What It Demonstrates
- Typed request and response messages.
- Broker request-response functions.
- Choosing a channel from an operation value.
Expected Result
A calculator API and UI that return sums and differences produced by separate services.
π§ Voyage
1. Model the Broker Messages
Letβs start with the information that travels between services. Every
calculation needs two numbers, so we will place them together in a
MathRequest.
@datatype
MathRequest
num1: Int
num2: Int
The worker sends one calculated number back. Wrapping it in
MathResponse gives the response a clear type and leaves room
to add more result information later.
MathResponse
resValue: Int
These two datatypes form a small contract: every math request contains two integers, and every successful response contains one integer.
2. Define the Calculator APIs
The public MathApi gives users three ways to calculate. The
first two methods have a fixed operation, while the third receives the
operation as an input.
@api
MathApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
generateSwagger = true
post /math/add/{a}/{b} addNumbers(a:Int, b:Int) : Int
post /math/sub/{a}/{b} subNumbers(a:Int, b:Int) : Int
get /math/calc/{a}/{b}/{op} calculate(a:Int, b:Int, op:String) : Int
Read an endpoint as verb path method(inputs) : output. For
example, addNumbers receives a and b
from the path and returns an integer.
The add and subtract services also expose their own small APIs. Those direct endpoints are useful for testing each worker independently, even though the main flow will reach the workers through the broker.
AddApi
post /math/add/{a}/{b} addNumbers(a:Int, b:Int) : Int
SubApi
post /math/sub/{a}/{b} subNumbers(a:Int, b:Int) : Int
3. Add Typed Broker Channels
A normal event is sent without waiting for a reply. A calculation is
different: the caller needs the answer. That makes
request-response a good fit for both math channels.
@broker
MathBroker
engine = nats
configType = MathBrokerConfig
math.add : MathRequest-MathResponse as request-response with timeout:1s
math.sub : MathRequest-MathResponse as request-response with timeout:1s
Each line describes one complete conversation:
math.addormath.subis the channel;MathRequest-MathResponsedefines input and output types;request-responsemeans the sender expects a reply;timeout:1slimits how long the broker waits.
4. Write the Math and Messaging Expressions
Calculate inside each worker
The responder receives the typed broker request, performs one operation, and places the answer in a typed response.
AddResponser
input: req:MathRequest
output: result:MathResponse
logic
result.resValue = req.num1 + req.num2
SubResponser
input: req:MathRequest
output: result:MathResponse
logic
result.resValue = req.num1 - req.num2
Turn a broker channel into a function
RequestHandler does not need to know which channel it calls.
It receives a requester function, creates MathRequest, calls
that function, and returns the value from MathResponse.
RequestHandler
input: a:Int & b:Int & requester:Func<MathRequest,MathResponse>
output: result:Int
logic
var req MathRequest
var res MathResponse
req.num1 = a
req.num2 = b
res = requester(req)
result = res.resValue
Choose a requester dynamically
The generic calculate endpoint adds one more idea. It receives both
requester functions and selects one from the op value.
CalculateHandler
input: a:Int & b:Int & op:String & addRequester:Func<MathRequest,MathResponse> & subRequester:Func<MathRequest,MathResponse>
When op is add, it calls
addRequester; when it is sub, it calls
subRequester. Both functions share the same signature, so
the surrounding expression can treat them consistently.
5. Wire Three Services
Prepare requester functions in MathService
The coordinating service declares two function aliases with the same request and response types as the broker channels.
MathService
use broker MathBroker
impl api MathApi as api on mathCfg.apiConfig.port
use function Func<MathRequest,MathResponse> as AddRequester
use function Func<MathRequest,MathResponse> as SubRequester
connect AddRequester -> MathBroker.math.add & timeout(3:s)
connect SubRequester -> MathBroker.math.sub & timeout(3:s)
These connections make the channels callable like regular functions. The service-level timeout gives each call its own three-second boundary.
Pass the functions into expressions
connect api.addNumbers -> RequestHandler(requester:AddRequester)
connect api.subNumbers -> RequestHandler(requester:SubRequester)
connect api.calculate -> CalculateHandler(
addRequester:AddRequester,
subRequester:SubRequester
)
Addition and subtraction reuse RequestHandler with different
requester functions. The calculate method receives both functions and
decides which one to invoke.
Answer the channels in worker services
AddService
use broker MathBroker
connect MathBroker.math.add -> AddResponser
SubService
use broker MathBroker
connect MathBroker.math.sub -> SubResponser
This completes the round trip: MathService sends a request,
one worker responds, and the result returns to the original API call.
6. Configure APIs, Broker Access, and Logging
All three services need an API port, broker connection settings, and logging. A shared config datatype keeps that shape consistent.
@config
@import config O.broker.nats.config@1.0.0 as BrokerConfig
@import config O.log.config@1.0.0 as LogConfig
ServiceConfig
apiConfig: ApiConfig
brokerConfig: BrokerConfig
logConfig: LogConfig
ApiConfig
port: Int (default=8080)
The broker itself uses MathBrokerConfig, while the UI uses a
smaller UiConfig. Each component receives only the settings
it needs.
7. Create a Simple Calculator Dashboard
The dashboard uses two forms: one for addition and one for subtraction.
Each form collects the same two integer fields, a and
b.
@dashboard
MathDashboard
title: Let's Calculate!
layout: SingleColumnLayout
Widget SumForm of type Form
title = Sum
fields:
- a Int
- b Int
buttons:
submit : Sum
cancel : Cancel
SubForm follows the same structure with a Sub button. The
matching field names allow the UI to pass both values directly to the
selected API method.
8. Connect the Forms to the Public API
The UI brings the calculator and About dashboards together, then uses
MathApi as its backend contract.
@ui
MathUi
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
configType: UiConfig
use dashboard MathDashboard as m
use dashboard AboutDashboard as a
use api MathApi
Each submit event connects to the corresponding public method. The
broker remains hidden behind MathService; the UI only needs
to understand the API.
connect m.SumForm.submit -> MathApi.addNumbers
connect m.SubForm.submit -> MathApi.subNumbers
9. Deploy the API, Workers, Broker, and UI
The deployment starts the broker first and makes the three services depend on it. Only the public math API needs to be exposed to clients.
@deploy
@import service P.broker.nats.docker@1.0.0 as NATS
MathDeploy
service MathService
replica 1
export 9096:MathService.api
dependsOn Broker
Broker
service NATS
Addition and subtraction each run with two replicas. Because workers consume broker requests, more than one instance can share the work without exposing extra public ports.
AddDeploy
service AddService
replica 2
dependsOn Broker
SubDeploy
service SubService
replica 2
dependsOn Broker
Finally, the UI is exposed on port 8085 and depends on the
public math service, whose API is available on port 9096.
MathUiDeploy
service MathUi
replica 1
export 8085:MathUi.config
dependsOn MathDeploy
10. Run and Follow a Calculation
- Validate and generate the complete example.
- Start the generated services by following their README.
- Open the UI at
localhost:8085. - Enter two numbers in the Sum form and submit it.
- Repeat the test with the Sub form.
- Call
/math/calc/{a}/{b}/addand then usesub.
While testing, follow one request mentally: UI β Math API β requester expression β typed broker channel β worker responder β Math API β UI. That round trip is the central lesson of the example.
11. Conclusion
You have built a small calculator, but the pattern applies to much more than arithmetic. A coordinating service can turn a typed broker channel into a function, pass that function into reusable logic, and let a specialized worker produce the response.
Afterwards, you should understand how to:
- model typed request and response messages;
- declare request-response broker channels;
- connect a function alias to a broker channel;
- inject requester functions into expressions;
- implement a channel with a worker responder;
- scale worker services independently with replicas;
- keep the broker hidden behind a simple public API and UI.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - documentation
# perspective:
# feature: math-service
# @ocean-meta-end
@info
name: Math App
version: 1.0.0
title: Math-app
subtitle: To perform your math
shortDescription: Answers all your math questions
description: This app answers your math questions
# @ocean-meta-start
# tags:
# - broker-request-response
# - datatype
# perspective:
# feature: math-service
# service: math-service
# @ocean-meta-end
@datatype
MathRequest
num1: Int
num2: Int
MathResponse
resValue: Int
# @ocean-meta-start
# tags:
# - add-api
# - rest-api
# perspective:
# feature: math-service
# service: add-service
# @ocean-meta-end
@api
AddApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
generateSwagger = true
post /math/add/{a}/{b} addNumbers(a:Int, b:Int) : Int
# @ocean-meta-start
# tags:
# - math-api
# - rest-api
# perspective:
# feature: math-service
# service: math-service
# @ocean-meta-end
@api
MathApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
generateSwagger = true
post /math/add/{a}/{b} addNumbers(a:Int, b:Int) : Int
post /math/sub/{a}/{b} subNumbers(a:Int, b:Int) : Int
get /math/calc/{a}/{b}/{op} calculate(a:Int, b:Int, op:String) : Int
# @ocean-meta-start
# tags:
# - sub-api
# - rest-api
# perspective:
# feature: math-service
# service: sub-service
# @ocean-meta-end
@api
SubApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
generateSwagger = true
post /math/sub/{a}/{b} subNumbers(a:Int, b:Int) : Int
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: math-service
# service: all
# @ocean-meta-end
@config
@import config O.broker.nats.config@1.0.0 as BrokerConfig
@import config O.log.config@1.0.0 as LogConfig
ServiceConfig
apiConfig: ApiConfig
brokerConfig: BrokerConfig
logConfig: LogConfig
ApiConfig
port: Int (default=8080)
MathBrokerConfig
brokerConfig : BrokerConfig
logConfig : LogConfig
UiConfig
port: Int (default=8080)
logConfig : LogConfig
# @ocean-meta-start
# tags:
# - broker-request-response
# - broker
# perspective:
# feature: math-service
# service: all
# @ocean-meta-end
@broker
MathBroker
@perspectives: version:0.1.0, lifestyle:stable
tags = primary, shared
engine = nats
configType = MathBrokerConfig
math.add : MathRequest-MathResponse as request-response with timeout:1s
math.sub : MathRequest-MathResponse as request-response with timeout:1s
# @ocean-meta-start
# tags:
# - expression
# perspective:
# feature: math-service
# service: all
# @ocean-meta-end
@expression
HandleAdd
input: a:Int & b:Int
output: result:Int
logic
result = a + b
AddResponser
input: req:MathRequest
output: result:MathResponse
logic
result.resValue = req.num1 + req.num2
HandleSub
input: a:Int & b:Int
output: result:Int
logic
result = a - b
SubResponser
input: req:MathRequest
output: result:MathResponse
logic
result.resValue = req.num1 - req.num2
RequestHandler
input: a:Int & b:Int & requester:Func<MathRequest,MathResponse>
output: result:Int
logic
var req MathRequest
var res MathResponse
req.num1 = a
req.num2 = b
res = requester(req)
result = res.resValue
CalculateHandler
input: a:Int & b:Int & op:String & addRequester:Func<MathRequest,MathResponse> & subRequester:Func<MathRequest,MathResponse>
output: result:Int
logic
var req MathRequest
var res MathResponse
req.num1 = a
req.num2 = b
if op=="add" then
res = addRequester(req)
end
if op=="sub" then
res = subRequester(req)
end
result = res.resValue
# @ocean-meta-start
# tags:
# - add-api
# - service
# perspective:
# feature: math-service
# service: add-service
# @ocean-meta-end
@service
AddService
use config ServiceConfig as addCfg
use broker MathBroker as broker
impl api AddApi as api on addCfg.apiConfig.port
use expression HandleAdd
use expression AddResponser
connect api.addNumbers -> HandleAdd
connect broker.math.add -> AddResponser
# @ocean-meta-start
# tags:
# - math-api
# - service
# perspective:
# feature: math-service
# service: math-service
# @ocean-meta-end
@service
MathService
use config ServiceConfig as mathCfg
use broker MathBroker as broker
impl api MathApi as api on mathCfg.apiConfig.port
use expression RequestHandler
use expression CalculateHandler
use function Func<MathRequest,MathResponse> as AddRequester
use function Func<MathRequest,MathResponse> as SubRequester
connect api.addNumbers -> RequestHandler(requester:AddRequester)
connect api.subNumbers -> RequestHandler(requester:SubRequester)
connect AddRequester -> broker.math.add & timeout(3:s)
connect SubRequester -> broker.math.sub & timeout(3:s)
connect api.calculate -> CalculateHandler(addRequester:AddRequester, subRequester:SubRequester)
# @ocean-meta-start
# tags:
# - sub-api
# - service
# perspective:
# feature: math-service
# service: sub-service
# @ocean-meta-end
@service
SubService
use config ServiceConfig as subCfg
use broker MathBroker as broker
impl api SubApi as api on subCfg.apiConfig.port
use expression HandleSub
use expression SubResponser
connect api.subNumbers -> HandleSub
connect broker.math.sub -> SubResponser
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: math-service
# service: all
# @ocean-meta-end
@deploy
Name: MathDeploy-PRD-2025-08
@import service P.broker.nats.docker@1.0.0 as NATS
MathDeploy
service MathService
replica 1
export 9096:MathService.api
dependsOn Broker
AddDeploy
service AddService
replica 2
#export 9097:AddService.api
dependsOn Broker
SubDeploy
service SubService
replica 2
#export 9098:SubService.api
dependsOn Broker
Broker
service NATS
MathUiDeploy
service MathUi
replica 1
export 8085:MathUi.config
dependsOn MathDeploy
# @ocean-meta-start
# tags:
# - ui
# - dashboard
# perspective:
# feature: math-service
# service: ui-service
# @ocean-meta-end
@dashboard
MathDashboard
title: Let's Calculate!
layout: SingleColumnLayout
Widget SumForm of type Form
title = Sum
fields:
- a Int
- b Int
buttons:
submit : Sum
cancel : Cancel
Widget SubForm of type Form
title = Sub
fields:
- a Int
- b Int
buttons:
submit : Sub
cancel : Cancel
AboutDashboard
title: About
layout: SingleColumnLayout
Widget AboutText of type Text
title = About Math App
subtitle = This app helps you to solve your math problems!
content = ver. 1.0.0
# @ocean-meta-start
# tags:
# - ui
# perspective:
# feature: math-service
# service: ui-service
# @ocean-meta-end
@ui
MathUi
@perspectives: version:0.1.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav Calculate dashboard = MathDashboard
nav About dashboard = AboutDashboard
header title = β Math App
header subtitle = To solve your math problems!
header align = center
footer title = β‘ Built with Ocean-lab
footer subtitle = Version 1.0.0
footer align = center
use dashboard MathDashboard as m
use dashboard AboutDashboard as a
use api MathApi
connect m.SumForm.submit -> MathApi.addNumbers
connect m.SubForm.submit -> MathApi.subNumbers
flowchart
u[User]
subgraph s[System]
a[API]
ms[[Math-service]]
as[[Add-service]]
ss[[Sub-service]]
b([Broker])
e[Expressions]
end
u -.communicates.-> a
a --> ms
ms <--request-response--> b
b <--> as
b <--> ss
as -.uses.-> e
ss -.uses.-> e