Order Exchange
Exchange orders with six partners over every combination of REST, SOAP, and gRPC, inbound and outbound, push and pull, mapping each partner's own format at the integration boundaries.
Exampleintegrationrestsoapgrpcpushpullmappingbrokeraggregationschedulingcontextuidashboard
π Horizon
Example at a Glance
Overview
Real integrations rarely meet one kind of partner. This exchange takes orders from three buyers and hands them on to three sellers, and every partner speaks its own API style, REST, SOAP, or gRPC, with its own order format. Each partner is reached both ways: it pushes to Ocean, and Ocean pulls from it. That makes twelve flows, one for every combination of style, direction, and push or pull.
Draft: the model follows the Ocean references, but the engine cannot yet generate the SOAP and gRPC outbound aggregates or the scheduled pulls. Everything else validates, generates, and compiles.
Architecture
Solid lines are pushes and dotted lines are pulls. Each label names the party whose timer starts the call. REST flows are green, SOAP flows pink, and gRPC flows indigo.
What It Demonstrates
@integrationβ an inbound and an outbound integration, each hosted by a thin service, with push and pull derived from their shape.@apiβ REST, SOAP, and gRPC APIs side by side, implemented by integrations and partners and used across them.@expressionβ a mapper for each partner format on the way in and on the way out, around one canonicalOrder.@scheduleβ scheduled connections and aggregates, each owned by the party that starts the call.@brokerβ an event topic as the queue between intake and processing.@contextβ the processor's orders, from which the six outboxes are derived.@dashboardand@uiβ a flow monitor, order and dispatch lists, and a search form.@configand@deployβ one port type per API style, so one service can serve all three.
Expected Result
The Flow Monitor lists all twelve flows. Inbound counts grow as buyers push and OrderIntake polls; once a minute OrderDispatch releases the processed batch, and the outbound counts grow as each seller is pushed to or polls. The Orders dashboard shows every order with the flow it came in on, the flow it goes out on, and its status. The Dispatched dashboard shows which seller got each order and how long it waited, and the Search dashboard finds the orders of any buyer or seller.
π§ Voyage
1. Problem and Constraints
Every partner has its own protocol, its own format, and its own idea of who calls whom. The exchange must accept pushes and run polls on the way in, make pushes and serve polls on the way out, and keep one canonical order inside. Push and pull are never declared in Ocean; they follow from which side implements an API and which side runs the schedule, so each timer lives with whoever starts the call.
2. Prerequisites
- Ocean UI with the SOAP and gRPC API styles and hosted integrations.
- A container runtime for the generated deployment.
- Recommended:
purchase-order-orchestrationfor integrations and aggregates, and the three gateway answer hubs for the REST, SOAP, and gRPC styles.
3. Example Structure
0018-order-exchange/
βββ 00-oex-info.ocn
βββ 01-oex-context.ocn
βββ 10-oex-datatype.ocn
βββ 20-oex-api-dispatch-grpc.ocn
βββ 20-oex-api-dispatch-rest.ocn
βββ 20-oex-api-dispatch-soap.ocn
βββ 20-oex-api-grpc-buyer.ocn
βββ 20-oex-api-grpc-seller.ocn
βββ 20-oex-api-intake-grpc.ocn
βββ 20-oex-api-intake-rest.ocn
βββ 20-oex-api-intake-soap.ocn
βββ 20-oex-api-order-processor.ocn
βββ 20-oex-api-rest-buyer.ocn
βββ 20-oex-api-rest-seller.ocn
βββ 20-oex-api-soap-buyer.ocn
βββ 20-oex-api-soap-seller.ocn
βββ 20-oex-broker.ocn
βββ 30-oex-config.ocn
βββ 40-oex-expression-buyers.ocn
βββ 40-oex-expression-dispatch-mappers.ocn
βββ 40-oex-expression-intake-mappers.ocn
βββ 40-oex-expression-processor.ocn
βββ 40-oex-expression-sellers.ocn
βββ 50-oex-integration-dispatch.ocn
βββ 50-oex-integration-intake.ocn
βββ 51-oex-service-grpc-buyer.ocn
βββ 51-oex-service-rest-buyer.ocn
βββ 51-oex-service-soap-buyer.ocn
βββ 52-oex-service-order-dispatch.ocn
βββ 52-oex-service-order-intake.ocn
βββ 52-oex-service-order-processor.ocn
βββ 53-oex-service-grpc-seller.ocn
βββ 53-oex-service-rest-seller.ocn
βββ 53-oex-service-soap-seller.ocn
βββ 55-oex-dashboard-dispatched.ocn
βββ 55-oex-dashboard-flows.ocn
βββ 55-oex-dashboard-orders.ocn
βββ 55-oex-dashboard-search.ocn
βββ 56-oex-ui.ocn
βββ 60-oex-deploy.ocn
βββ architecture.mmd
βββ services.mmd
βββ expression/order-exchange.go
βββ example-info/example-info.html
4. Services and the APIs Between Them
The architecture shows the twelve flows; this view shows what carries them. Every API sits between the service that calls it and the service that implements it. Arrows follow the orders: solid lines are pushes, dotted lines are pulls, and each label names the operation. A thick line joins an API to the service that implements it.
Buyers and sellers each implement one API and call another, which is how every partner is reached both ways. OrderIntakeService and OrderDispatchService implement one API per style through their hosted integrations. Only orders.inbound goes through the broker.
5. Give Every Partner Its Own Format
10-oex-datatype.ocn defines the canonical
Order and one format per partner, each with its own
vocabulary. The gRPC
buyer and the SOAP seller also nest their line item.
RestBuyerOrder GrpcBuyerOrder
orderId : String code : String
product : String line : GrpcBuyerLine
quantity : Int account : String
buyer : String channel : String
channel : String
Mappers convert at the boundary, in
40-oex-expression-intake-mappers.ocn and
40-oex-expression-dispatch-mappers.ocn:
MapRestBuyerOrder, MapSoapBuyerOrder, and
MapGrpcBuyerOrder on the way in, and
MapToRestSellerOrders, MapToSoapSellerOrders,
and MapToGrpcSellerOrders on the way out. Their Go bodies
are in expression/order-exchange.go.
6. Take Orders In: Push and Pull
OrderIntake implements one intake API per style, which the
buyers push to, and uses each buyer's API, which it polls. Both paths
map the buyer's format and publish the result.
impl api IntakeSoapApi as soapIn on cfg.soapApiConfig.port
use api SoapBuyerApi as soapBuyer
use expression MapSoapBuyerOrder as fromSoapBuyer
# Push: the buyer calls on its own timer.
connect soapIn.submitOrder -fromSoapBuyer-> broker.orders.inbound
# Pull: OrderIntake polls on its timer.
connect soapBuyer.nextOrder -fromSoapBuyer-> broker.orders.inbound schedule every 36s
The buyer's push is its own scheduled connection, in
51-oex-service-soap-buyer.ocn:
connect push -> intake.submitOrder schedule every 35s.
7. Queue and Process
orders.inbound is an event topic, the queue between intake
and processing. OrderProcessor consumes it and keeps every
order in ProcessorContext with an outbound flow, assigned
in turn across the six seller flows, and a status.
connect broker.orders.inbound -> process()
An integration keeps no state, so the outboxes live here: the released
orders of each outbound flow. OrderProcessorApi releases the
processed batch and hands out each flow's outbox.
8. Hand Orders On: Push and Pull
OrderDispatch releases the processed batch every sixty
seconds, then runs one aggregate per outbound flow. A push aggregate
runs on OrderDispatch's own schedule and delivers to the seller; a pull
aggregate runs when the seller polls the dispatch API and responds.
aggregate PushToSoapSeller
schedule every 37s
result: List<OrderBatch>
collect processor.takeSoapPushOrders
deliver soapSeller.receiveOrders using toSoapSeller
aggregate ServeSoapSeller
trigger soapOut.takeOrders
result: List<OrderBatch>
collect processor.takeSoapPullOrders
respond using toSoapSeller
The seller's poll is its own scheduled connection, in
53-oex-service-soap-seller.ocn:
connect dispatch.takeOrders -> accept() schedule every 38s.
9. One Port per API Style
OrderIntakeService and OrderDispatchService
each serve REST, SOAP, and gRPC at once, so each style has its own port
type and default: ApiConfig (8080),
SoapApiConfig (8081), and GrpcApiConfig
(8082). A config field is named after its type, as the generated
services expect.
CommonServiceConfig
apiConfig : ApiConfig
soapApiConfig : SoapApiConfig
grpcApiConfig : GrpcApiConfig
brokerConfig : BrokerConfig
logConfig : LogConfig
10. Watch the Flows and Track the Orders
OrderExchangeUi reads OrderProcessorApi in
four dashboards:
- Flow Monitor: one row per flow, with who starts it, its schedule, and its order count.
- Orders: every order, newest first, with its buyer and seller, flows, status, and times.
- Dispatched: the orders handed on to sellers, with how long each waited in the exchange.
- Search: a form that finds orders by buyer, by seller, or both.
OrderProcessor stamps each order when it accepts it and when its outbox is taken, so every order can be followed from buyer to seller. The search form is backed by an operation that takes the form's datatype:
OrderQuery
buyer : String
seller : String
post /orders/search searchOrders(query:OrderQuery) : List<OrderRow>
connect search.OrderSearchForm.submit -> OrderProcessorApi.searchOrders
11. Validate, Generate, and Run
- Open the
Order Exchangeexample in Ocean UI and validate it. - Once the engine supports SOAP and gRPC aggregates and scheduled API calls, generate the complete model.
- Follow the generated
Readme.mdto start the broker, services, and UI. - Open the UI at
localhost:8086.
12. Verify the Result
- Within a minute, all six inbound rows on the Flow Monitor count orders.
- After the first batch release, at sixty seconds, the six outbound rows start counting as well.
- On the Orders dashboard, orders move from processed to released to dispatched, spread evenly over the six outbound flows.
- The Dispatched dashboard fills with orders from all three buyers, each with its seller and waiting time.
- Search for seller
SoapSeller: every result goes out on a SOAP flow. Add buyerGrpcBuyerto narrow it to orders that also came in over gRPC. - Each seller's log reports every delivery, pushed or pulled.
13. Experiments
- Change a buyer's push interval and watch its inbound row count faster or slower.
- Stop
OrderProcessor: intake keeps publishing, and the orders wait in the broker topic. - Add a field to
SoapBuyerOrderand carry it through its mapper toOrder. - Replace the round-robin assignment in
AcceptOrderwith routing by product.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - integration
# - rest-api
# - soap-api
# - grpc-api
# - broker
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# @ocean-meta-end
@info
name: Order Exchange
version: 1.0.0
title: Order Exchange
subtitle: REST, SOAP, and gRPC partners, inbound and outbound, push and pull
shortDescription: A complete integration example that exchanges orders with six partners over every combination of API style, direction, and push or pull.
description:
Overview
Order Exchange receives orders from three buyers and hands them on to three sellers. Each partner speaks one API style: REST, SOAP, or gRPC. Every partner is reached in both ways: it pushes to Ocean, and Ocean pulls from it. That gives twelve flows, one for each combination of style, direction, and push or pull.
Inbound
OrderIntake is an inbound integration. Buyers push orders to the three intake APIs it implements, and it polls each buyer's own API on a schedule. Every order is mapped from the buyer's own format to the canonical Order and published to the orders.inbound broker topic.
Processing
OrderProcessor consumes orders.inbound, keeps every order, and assigns each one an outbound flow, taking turns across the six seller flows.
Outbound
OrderDispatch is an outbound integration. Every sixty seconds it releases the processed batch. It then pushes released orders to each seller on a schedule, and serves them to sellers that poll the three dispatch APIs it implements. Every order is mapped to the seller's own format on the way out.
Monitoring
The Flow Monitor dashboard lists all twelve flows with the number of orders each has carried, and the Orders dashboard shows every order with its inbound and outbound flow.
# @ocean-meta-start
# tags:
# - context
# - order
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-processor
# @ocean-meta-end
@context
# Every order OrderProcessor has received, with its outbound flow and status.
# The outbound flows' outboxes are the released orders of each flow.
ProcessorContext
orders : List<Order>
# @ocean-meta-start
# tags:
# - datatype
# - order
# - canonical
# - partner-format
# - dashboard
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: shared
# @ocean-meta-end
@datatype
# The canonical order used inside the exchange. sourceId is the buyer's own
# order identifier (RestBuyerOrder.orderId, SoapBuyerOrder.reference, or
# GrpcBuyerOrder.code), kept so an order can be found by either identifier.
# inboundFlow and outboundFlow name one of the twelve flows, such as
# "REST push" or "gRPC pull"; status is processed, released, or dispatched.
# receivedAt is set when OrderProcessor accepts the order, dispatchedAt when
# its outbox is taken.
Order
id pattern ORD-UUID
sourceId : String
product : String
quantity : Int
buyer : String
inboundFlow : String
outboundFlow : String
status : String
receivedAt : DateTime
dispatchedAt : *DateTime
# RestBuyer's own order format. OrderIntake maps it to Order.
# channel names the inbound flow, such as "REST push".
RestBuyerOrder
orderId : String
product : String
quantity : Int
buyer : String
channel : String
# SoapBuyer's own order format. OrderIntake maps it to Order.
# channel names the inbound flow, such as "SOAP pull".
SoapBuyerOrder
reference : String
item : String
units : Int
party : String
channel : String
# The line item nested in GrpcBuyerOrder.
GrpcBuyerLine
sku : String
count : Int
# GrpcBuyer's own order format, with a nested line item. OrderIntake maps it
# to Order. channel names the inbound flow, such as "gRPC push".
GrpcBuyerOrder
code : String
line : GrpcBuyerLine
account : String
channel : String
# RestSeller's own order format. OrderDispatch maps Order to it.
# origin names the inbound flow the order arrived through.
RestSellerOrder
saleId : String
article : String
amount : Int
customer : String
origin : String
# The line item nested in SoapSellerOrder.
SoapSellerLine
goods : String
pieces : Int
# SoapSeller's own order format, with a nested line item. OrderDispatch maps
# Order to it. origin names the inbound flow the order arrived through.
SoapSellerOrder
ticket : String
line : SoapSellerLine
client : String
origin : String
# GrpcSeller's own order format. OrderDispatch maps Order to it.
# origin names the inbound flow the order arrived through.
GrpcSellerOrder
uid : String
catalogNumber : String
qty : Int
purchaser : String
origin : String
# One outbound flow's outbox, as OrderProcessor hands it to OrderDispatch's
# aggregates.
OrderBatch
orders : List<Order>
# The size of a batch OrderProcessor released into the outboxes.
BatchRelease
released : Int
# One row of the Flow Monitor: one of the twelve flows.
FlowRow
id : String
flow : String
direction : String
style : String
mode : String
startedBy : String
schedule : String
orders : Int
# One order and its journey through the exchange, as listed on the Orders
# dashboard and returned by an order search. seller is the seller its outbound
# flow leads to; dispatchedAt is empty until the order is dispatched.
OrderRow
id : String
sourceId : String
product : String
quantity : Int
buyer : String
seller : String
inboundFlow : String
outboundFlow : String
status : String
receivedAt : String
dispatchedAt : String
# One row of the Dispatched dashboard: a dispatched order, the seller that got
# it, and how long it waited in the exchange.
DispatchedRow
id : String
sourceId : String
product : String
quantity : Int
buyer : String
seller : String
inboundFlow : String
outboundFlow : String
receivedAt : String
dispatchedAt : String
waited : String
# The Search dashboard's form: find orders by their id, by buyer, by seller,
# or any combination. An empty field matches anything; matching ignores case.
# id matches either the exchange's own ORD- id or the buyer's own order id.
OrderQuery
id : String
buyer : String
seller : String
# @ocean-meta-start
# tags:
# - api
# - grpc-api
# - dispatch
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-dispatch-service
# @ocean-meta-end
@api
DispatchGrpcApi style:grpc
engine = connect
configType = GrpcApiConfig
version = 1.0.0
description = Implemented by OrderDispatch; GrpcSeller polls it for its released orders.
protoPackage = oceanlab.examples.orderexchange.dispatch.v1
reflection = true
rpc takeOrders() : List<GrpcSellerOrder>
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - dispatch
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-dispatch-service
# @ocean-meta-end
@api
DispatchRestApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
description = Implemented by OrderDispatch; RestSeller polls it for its released orders.
basePath = /dispatch
generateSwagger = true
get /orders takeOrders() : List<RestSellerOrder>
# @ocean-meta-start
# tags:
# - api
# - soap-api
# - dispatch
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-dispatch-service
# @ocean-meta-end
@api
DispatchSoapApi style:soap
engine = soap
configType = SoapApiConfig
version = 1.0.0
description = Implemented by OrderDispatch; SoapSeller polls it for its released orders.
namespace = https://ocean-lab.ai/examples/order-exchange/dispatch/v1
endpointPath = /dispatch
soapVersion = 1.2
operation takeOrders() : List<SoapSellerOrder>
# @ocean-meta-start
# tags:
# - api
# - grpc-api
# - buyer
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: grpc-buyer
# @ocean-meta-end
@api
GrpcBuyerApi style:grpc
engine = connect
configType = GrpcApiConfig
version = 1.0.0
description = Implemented by GrpcBuyer; OrderIntake polls it for the next order.
protoPackage = oceanlab.examples.orderexchange.buyer.v1
reflection = true
rpc nextOrder() : GrpcBuyerOrder
# @ocean-meta-start
# tags:
# - api
# - grpc-api
# - seller
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: grpc-seller
# @ocean-meta-end
@api
GrpcSellerApi style:grpc
engine = connect
configType = GrpcApiConfig
version = 1.0.0
description = Implemented by GrpcSeller; OrderDispatch pushes released orders to it.
protoPackage = oceanlab.examples.orderexchange.seller.v1
reflection = true
rpc receiveOrders(orders:List<GrpcSellerOrder>) : _
# @ocean-meta-start
# tags:
# - api
# - grpc-api
# - intake
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-intake-service
# @ocean-meta-end
@api
IntakeGrpcApi style:grpc
engine = connect
configType = GrpcApiConfig
version = 1.0.0
description = Implemented by OrderIntake; the gRPC buyer pushes its orders here.
protoPackage = oceanlab.examples.orderexchange.intake.v1
reflection = true
rpc submitOrder(order:GrpcBuyerOrder) : _
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - intake
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-intake-service
# @ocean-meta-end
@api
IntakeRestApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
description = Implemented by OrderIntake; the REST buyer pushes its orders here.
basePath = /intake
generateSwagger = true
post /orders submitOrder(order:RestBuyerOrder) : _
# @ocean-meta-start
# tags:
# - api
# - soap-api
# - intake
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-intake-service
# @ocean-meta-end
@api
IntakeSoapApi style:soap
engine = soap
configType = SoapApiConfig
version = 1.0.0
description = Implemented by OrderIntake; the SOAP buyer pushes its orders here.
namespace = https://ocean-lab.ai/examples/order-exchange/intake/v1
endpointPath = /intake
soapVersion = 1.2
operation submitOrder(order:SoapBuyerOrder) : _
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - processor
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-processor
# @ocean-meta-end
@api
OrderProcessorApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
description = Internal API of OrderProcessor, used by OrderDispatch and the UI.
basePath = /processor
generateSwagger = true
# Moves every processed order into its outbound flow's outbox.
post /batches/release releaseBatch() : BatchRelease
# Each take returns the released orders of one outbound flow and marks
# them dispatched.
post /outbox/rest-push/take takeRestPushOrders() : OrderBatch
post /outbox/rest-pull/take takeRestPullOrders() : OrderBatch
post /outbox/soap-push/take takeSoapPushOrders() : OrderBatch
post /outbox/soap-pull/take takeSoapPullOrders() : OrderBatch
post /outbox/grpc-push/take takeGrpcPushOrders() : OrderBatch
post /outbox/grpc-pull/take takeGrpcPullOrders() : OrderBatch
# Monitoring. Each list is paginated: Page<T> carries the true total
# alongside its page of rows, so the UI knows when it has reached the
# last page.
get /flows/{offset}/{limit} listFlows(offset:Int, limit:Int) : Page<FlowRow>
get /orders/{offset}/{limit} listOrderRows(offset:Int, limit:Int) : Page<OrderRow>
get /dispatched/{offset}/{limit} listDispatchedRows(offset:Int, limit:Int) : Page<DispatchedRow>
post /orders/search searchOrders(query:OrderQuery) : List<OrderRow>
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - buyer
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: rest-buyer
# @ocean-meta-end
@api
RestBuyerApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
description = Implemented by RestBuyer; OrderIntake polls it for the next order.
basePath = /buyer
generateSwagger = true
get /orders/next nextOrder() : RestBuyerOrder
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - seller
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: rest-seller
# @ocean-meta-end
@api
RestSellerApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
description = Implemented by RestSeller; OrderDispatch pushes released orders to it.
basePath = /seller
generateSwagger = true
post /orders receiveOrders(orders:List<RestSellerOrder>) : _
# @ocean-meta-start
# tags:
# - api
# - soap-api
# - buyer
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: soap-buyer
# @ocean-meta-end
@api
SoapBuyerApi style:soap
engine = soap
configType = SoapApiConfig
version = 1.0.0
description = Implemented by SoapBuyer; OrderIntake polls it for the next order.
namespace = https://ocean-lab.ai/examples/order-exchange/buyer/v1
endpointPath = /buyer
soapVersion = 1.2
operation nextOrder() : SoapBuyerOrder
# @ocean-meta-start
# tags:
# - api
# - soap-api
# - seller
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: soap-seller
# @ocean-meta-end
@api
SoapSellerApi style:soap
engine = soap
configType = SoapApiConfig
version = 1.0.0
description = Implemented by SoapSeller; OrderDispatch pushes released orders to it.
namespace = https://ocean-lab.ai/examples/order-exchange/seller/v1
endpointPath = /seller
soapVersion = 1.2
operation receiveOrders(orders:List<SoapSellerOrder>) : _
# @ocean-meta-start
# tags:
# - broker
# - nats
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-exchange-broker
# @ocean-meta-end
@broker
OrderExchangeBroker
@perspectives: version:1.0.0, lifestyle:stable
engine = nats
configType = OrderExchangeBrokerConfig
tags = primary, shared
# The queue between intake and processing: one event per order.
orders.inbound : Order as event
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: shared
# @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
# One port type per API style, so a service that serves all three styles, such
# as OrderIntakeService, gets three distinct default ports.
ApiConfig
port : Int (default=8080)
SoapApiConfig
port : Int (default=8081)
GrpcApiConfig
port : Int (default=8082)
CommonServiceConfig
apiConfig : ApiConfig
soapApiConfig : SoapApiConfig
grpcApiConfig : GrpcApiConfig
brokerConfig : BrokerConfig
logConfig : LogConfig
OrderExchangeBrokerConfig
brokerConfig : BrokerConfig
logConfig : LogConfig
UiConfig
port : Int (default=8080)
logConfig : LogConfig
# @ocean-meta-start
# tags:
# - expression
# - buyer
# - generator
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: buyers
# @ocean-meta-end
@expression
# Buyers: a new order in each buyer's own format. The channel records which
# of the six inbound flows carries it.
NewRestBuyerOrder
input: channel:String
output: order:RestBuyerOrder
external
source: order-exchange.go
method: NewRestBuyerOrder
NewSoapBuyerOrder
input: channel:String
output: order:SoapBuyerOrder
external
source: order-exchange.go
method: NewSoapBuyerOrder
NewGrpcBuyerOrder
input: channel:String
output: order:GrpcBuyerOrder
external
source: order-exchange.go
method: NewGrpcBuyerOrder
GenerateRestPushOrder
input: _
output: order:RestBuyerOrder
logic
order = NewRestBuyerOrder("REST push")
GenerateRestPullOrder
input: _
output: order:RestBuyerOrder
logic
order = NewRestBuyerOrder("REST pull")
GenerateSoapPushOrder
input: _
output: order:SoapBuyerOrder
logic
order = NewSoapBuyerOrder("SOAP push")
GenerateSoapPullOrder
input: _
output: order:SoapBuyerOrder
logic
order = NewSoapBuyerOrder("SOAP pull")
GenerateGrpcPushOrder
input: _
output: order:GrpcBuyerOrder
logic
order = NewGrpcBuyerOrder("gRPC push")
GenerateGrpcPullOrder
input: _
output: order:GrpcBuyerOrder
logic
order = NewGrpcBuyerOrder("gRPC pull")
# @ocean-meta-start
# tags:
# - expression
# - mapping
# - outbound
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-dispatch-service
# @ocean-meta-end
@expression
# OrderDispatch mappers: the collected outbox batches to each seller's format.
MapToRestSellerOrders
input: batches:List<OrderBatch>
output: orders:List<RestSellerOrder>
external
source: order-exchange.go
method: MapToRestSellerOrders
MapToSoapSellerOrders
input: batches:List<OrderBatch>
output: orders:List<SoapSellerOrder>
external
source: order-exchange.go
method: MapToSoapSellerOrders
MapToGrpcSellerOrders
input: batches:List<OrderBatch>
output: orders:List<GrpcSellerOrder>
external
source: order-exchange.go
method: MapToGrpcSellerOrders
# @ocean-meta-start
# tags:
# - expression
# - mapping
# - inbound
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-intake-service
# @ocean-meta-end
@expression
# OrderIntake mappers: each buyer's format to the canonical Order.
MapRestBuyerOrder
input: source:RestBuyerOrder
output: order:Order
external
source: order-exchange.go
method: MapRestBuyerOrder
MapSoapBuyerOrder
input: source:SoapBuyerOrder
output: order:Order
external
source: order-exchange.go
method: MapSoapBuyerOrder
MapGrpcBuyerOrder
input: source:GrpcBuyerOrder
output: order:Order
external
source: order-exchange.go
method: MapGrpcBuyerOrder
# @ocean-meta-start
# tags:
# - expression
# - processor
# - context
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-processor
# @ocean-meta-end
@expression
# OrderProcessor: helpers over the list of orders it keeps, and the behavior
# behind its subscription and API.
AcceptOrder
input: orders:List<Order> & order:Order
output: result:List<Order>
external
source: order-exchange.go
method: AcceptOrder
CountOrdersWithStatus
input: orders:List<Order> & status:String
output: count:Int
external
source: order-exchange.go
method: CountOrdersWithStatus
ReleaseProcessedOrders
input: orders:List<Order>
output: result:List<Order>
external
source: order-exchange.go
method: ReleaseProcessedOrders
ReleasedOrdersOf
input: orders:List<Order> & flow:String
output: result:List<Order>
external
source: order-exchange.go
method: ReleasedOrdersOf
MarkDispatched
input: orders:List<Order> & flow:String
output: result:List<Order>
external
source: order-exchange.go
method: MarkDispatched
ToFlowRows
input: orders:List<Order> & offset:Int & limit:Int
output: rows:Page<FlowRow>
external
source: order-exchange.go
method: ToFlowRows
ToOrderRows
input: orders:List<Order> & offset:Int & limit:Int
output: rows:Page<OrderRow>
external
source: order-exchange.go
method: ToOrderRows
ToDispatchedRows
input: orders:List<Order> & offset:Int & limit:Int
output: rows:Page<DispatchedRow>
external
source: order-exchange.go
method: ToDispatchedRows
FindOrders
input: orders:List<Order> & query:OrderQuery
output: rows:List<OrderRow>
external
source: order-exchange.go
method: FindOrders
ProcessOrder
context: ctx:*ProcessorContext
input: order:Order
logic
ctx.orders = AcceptOrder(ctx.orders, order)
ReleaseBatch
context: ctx:*ProcessorContext
input: _
output: release:BatchRelease
logic
var released BatchRelease
released.released = CountOrdersWithStatus(ctx.orders, "processed")
ctx.orders = ReleaseProcessedOrders(ctx.orders)
release = released
TakeRestPushOrders
context: ctx:*ProcessorContext
input: _
output: batch:OrderBatch
logic
var outbox OrderBatch
outbox.orders = ReleasedOrdersOf(ctx.orders, "REST push")
ctx.orders = MarkDispatched(ctx.orders, "REST push")
batch = outbox
TakeRestPullOrders
context: ctx:*ProcessorContext
input: _
output: batch:OrderBatch
logic
var outbox OrderBatch
outbox.orders = ReleasedOrdersOf(ctx.orders, "REST pull")
ctx.orders = MarkDispatched(ctx.orders, "REST pull")
batch = outbox
TakeSoapPushOrders
context: ctx:*ProcessorContext
input: _
output: batch:OrderBatch
logic
var outbox OrderBatch
outbox.orders = ReleasedOrdersOf(ctx.orders, "SOAP push")
ctx.orders = MarkDispatched(ctx.orders, "SOAP push")
batch = outbox
TakeSoapPullOrders
context: ctx:*ProcessorContext
input: _
output: batch:OrderBatch
logic
var outbox OrderBatch
outbox.orders = ReleasedOrdersOf(ctx.orders, "SOAP pull")
ctx.orders = MarkDispatched(ctx.orders, "SOAP pull")
batch = outbox
TakeGrpcPushOrders
context: ctx:*ProcessorContext
input: _
output: batch:OrderBatch
logic
var outbox OrderBatch
outbox.orders = ReleasedOrdersOf(ctx.orders, "gRPC push")
ctx.orders = MarkDispatched(ctx.orders, "gRPC push")
batch = outbox
TakeGrpcPullOrders
context: ctx:*ProcessorContext
input: _
output: batch:OrderBatch
logic
var outbox OrderBatch
outbox.orders = ReleasedOrdersOf(ctx.orders, "gRPC pull")
ctx.orders = MarkDispatched(ctx.orders, "gRPC pull")
batch = outbox
ListFlows
context: ctx:*ProcessorContext
input: offset:Int & limit:Int
output: rows:Page<FlowRow>
logic
rows = ToFlowRows(ctx.orders, offset, limit)
ListOrderRows
context: ctx:*ProcessorContext
input: offset:Int & limit:Int
output: rows:Page<OrderRow>
logic
rows = ToOrderRows(ctx.orders, offset, limit)
ListDispatchedRows
context: ctx:*ProcessorContext
input: offset:Int & limit:Int
output: rows:Page<DispatchedRow>
logic
rows = ToDispatchedRows(ctx.orders, offset, limit)
SearchOrders
context: ctx:*ProcessorContext
input: query:OrderQuery
output: rows:List<OrderRow>
logic
rows = FindOrders(ctx.orders, query)
# @ocean-meta-start
# tags:
# - expression
# - seller
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: sellers
# @ocean-meta-end
@expression
# Sellers: acknowledge a delivery, pushed or pulled, in the log.
AcceptRestSellerOrders
input: orders:List<RestSellerOrder>
external
source: order-exchange.go
method: AcceptRestSellerOrders
AcceptSoapSellerOrders
input: orders:List<SoapSellerOrder>
external
source: order-exchange.go
method: AcceptSoapSellerOrders
AcceptGrpcSellerOrders
input: orders:List<GrpcSellerOrder>
external
source: order-exchange.go
method: AcceptGrpcSellerOrders
# @ocean-meta-start
# tags:
# - integration
# - outbound
# - push
# - pull
# - aggregation
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-dispatch-service
# @ocean-meta-end
@integration
# Hands processed orders on to the sellers. Every sixty seconds it releases the
# processed batch into the six outboxes OrderProcessor keeps, one per outbound
# flow. It pushes each push flow's outbox to its seller on a schedule, and
# serves each pull flow's outbox to the seller that polls the dispatch API
# this integration implements. Orders are mapped to each seller's format.
OrderDispatch
@perspectives: version:1.0.0, lifestyle:stable
direction: out
use config CommonServiceConfig as cfg
impl api DispatchRestApi as restOut on cfg.apiConfig.port
impl api DispatchSoapApi as soapOut on cfg.soapApiConfig.port
impl api DispatchGrpcApi as grpcOut on cfg.grpcApiConfig.port
use api OrderProcessorApi as processor
use api RestSellerApi as restSeller
use api SoapSellerApi as soapSeller
use api GrpcSellerApi as grpcSeller
use expression MapToRestSellerOrders as toRestSeller
use expression MapToSoapSellerOrders as toSoapSeller
use expression MapToGrpcSellerOrders as toGrpcSeller
# Batch pull: release the processed batch into the outboxes.
aggregate ReleaseBatch
schedule every 60s
result: List<BatchRelease>
collect processor.releaseBatch
# Push: this integration delivers each outbox to its seller on a timer.
aggregate PushToRestSeller
schedule every 32s
result: List<OrderBatch>
collect processor.takeRestPushOrders
deliver restSeller.receiveOrders using toRestSeller
aggregate PushToSoapSeller
schedule every 37s
result: List<OrderBatch>
collect processor.takeSoapPushOrders
deliver soapSeller.receiveOrders using toSoapSeller
aggregate PushToGrpcSeller
schedule every 42s
result: List<OrderBatch>
collect processor.takeGrpcPushOrders
deliver grpcSeller.receiveOrders using toGrpcSeller
# Pull: each seller polls its dispatch API on its own timer.
aggregate ServeRestSeller
trigger restOut.takeOrders
result: List<OrderBatch>
collect processor.takeRestPullOrders
respond using toRestSeller
aggregate ServeSoapSeller
trigger soapOut.takeOrders
result: List<OrderBatch>
collect processor.takeSoapPullOrders
respond using toSoapSeller
aggregate ServeGrpcSeller
trigger grpcOut.takeOrders
result: List<OrderBatch>
collect processor.takeGrpcPullOrders
respond using toGrpcSeller
# @ocean-meta-start
# tags:
# - integration
# - inbound
# - push
# - pull
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-intake-service
# @ocean-meta-end
@integration
# Brings every buyer's orders into the exchange. Buyers push to the three
# intake APIs this integration implements; it pulls from the buyers' own APIs
# on a schedule. Each order is mapped from the buyer's format to Order and
# published to orders.inbound.
OrderIntake
@perspectives: version:1.0.0, lifestyle:stable
direction: in
use config CommonServiceConfig as cfg
impl api IntakeRestApi as restIn on cfg.apiConfig.port
impl api IntakeSoapApi as soapIn on cfg.soapApiConfig.port
impl api IntakeGrpcApi as grpcIn on cfg.grpcApiConfig.port
use api RestBuyerApi as restBuyer
use api SoapBuyerApi as soapBuyer
use api GrpcBuyerApi as grpcBuyer
use broker OrderExchangeBroker as broker
use expression MapRestBuyerOrder as fromRestBuyer
use expression MapSoapBuyerOrder as fromSoapBuyer
use expression MapGrpcBuyerOrder as fromGrpcBuyer
# Push: each buyer calls its intake API on its own timer.
connect restIn.submitOrder -fromRestBuyer-> broker.orders.inbound
connect soapIn.submitOrder -fromSoapBuyer-> broker.orders.inbound
connect grpcIn.submitOrder -fromGrpcBuyer-> broker.orders.inbound
# Pull: this integration polls each buyer on its own timer.
connect restBuyer.nextOrder -fromRestBuyer-> broker.orders.inbound schedule every 31s
connect soapBuyer.nextOrder -fromSoapBuyer-> broker.orders.inbound schedule every 36s
connect grpcBuyer.nextOrder -fromGrpcBuyer-> broker.orders.inbound schedule every 41s
# @ocean-meta-start
# tags:
# - service
# - buyer
# - partner
# - scheduling
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: grpc-buyer
# @ocean-meta-end
@service
# Sends orders both ways: pushes to IntakeGrpcApi on its own timer, and serves
# its next order to OrderIntake when polled.
GrpcBuyer
use config CommonServiceConfig as cfg
impl api GrpcBuyerApi as api on cfg.grpcApiConfig.port
use api IntakeGrpcApi as intake
use expression GenerateGrpcPushOrder as push
use expression GenerateGrpcPullOrder as pull
connect push -> intake.submitOrder schedule every 40s
connect api.nextOrder -> pull
# @ocean-meta-start
# tags:
# - service
# - buyer
# - partner
# - scheduling
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: rest-buyer
# @ocean-meta-end
@service
# Sends orders both ways: pushes to IntakeRestApi on its own timer, and serves
# its next order to OrderIntake when polled.
RestBuyer
use config CommonServiceConfig as cfg
impl api RestBuyerApi as api on cfg.apiConfig.port
use api IntakeRestApi as intake
use expression GenerateRestPushOrder as push
use expression GenerateRestPullOrder as pull
connect push -> intake.submitOrder schedule every 30s
connect api.nextOrder -> pull
# @ocean-meta-start
# tags:
# - service
# - buyer
# - partner
# - scheduling
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: soap-buyer
# @ocean-meta-end
@service
# Sends orders both ways: pushes to IntakeSoapApi on its own timer, and serves
# its next order to OrderIntake when polled.
SoapBuyer
use config CommonServiceConfig as cfg
impl api SoapBuyerApi as api on cfg.soapApiConfig.port
use api IntakeSoapApi as intake
use expression GenerateSoapPushOrder as push
use expression GenerateSoapPullOrder as pull
connect push -> intake.submitOrder schedule every 35s
connect api.nextOrder -> pull
# @ocean-meta-start
# tags:
# - service
# - integration-host
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-dispatch-service
# @ocean-meta-end
@service
# A thin host: supplies the process and deployment boundary for OrderDispatch.
OrderDispatchService
impl integration OrderDispatch as dispatch
# @ocean-meta-start
# tags:
# - service
# - integration-host
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-intake-service
# @ocean-meta-end
@service
# A thin host: supplies the process and deployment boundary for OrderIntake.
OrderIntakeService
impl integration OrderIntake as intake
# @ocean-meta-start
# tags:
# - service
# - processor
# - broker
# - context
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-processor
# @ocean-meta-end
@service
# Consumes orders.inbound and keeps every order with its outbound flow and
# status. The outboxes OrderDispatch reads are its released orders.
OrderProcessor
use config CommonServiceConfig as cfg
impl api OrderProcessorApi as api on cfg.apiConfig.port
use broker OrderExchangeBroker as broker
use context ProcessorContext as processorCtx
use expression ProcessOrder as process
use expression ReleaseBatch as release
use expression TakeRestPushOrders as takeRestPush
use expression TakeRestPullOrders as takeRestPull
use expression TakeSoapPushOrders as takeSoapPush
use expression TakeSoapPullOrders as takeSoapPull
use expression TakeGrpcPushOrders as takeGrpcPush
use expression TakeGrpcPullOrders as takeGrpcPull
use expression ListFlows as flows
use expression ListOrderRows as rows
use expression ListDispatchedRows as dispatchedRows
use expression SearchOrders as search
connect broker.orders.inbound -> process()
connect api.releaseBatch -> release()
connect api.takeRestPushOrders -> takeRestPush()
connect api.takeRestPullOrders -> takeRestPull()
connect api.takeSoapPushOrders -> takeSoapPush()
connect api.takeSoapPullOrders -> takeSoapPull()
connect api.takeGrpcPushOrders -> takeGrpcPush()
connect api.takeGrpcPullOrders -> takeGrpcPull()
connect api.listFlows -> flows
connect api.listOrderRows -> rows
connect api.listDispatchedRows -> dispatchedRows
connect api.searchOrders -> search
# @ocean-meta-start
# tags:
# - service
# - seller
# - partner
# - scheduling
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: grpc-seller
# @ocean-meta-end
@service
# Receives orders both ways: OrderDispatch pushes to its API on a timer, and it
# polls DispatchGrpcApi on its own timer.
GrpcSeller
use config CommonServiceConfig as cfg
impl api GrpcSellerApi as api on cfg.grpcApiConfig.port
use api DispatchGrpcApi as dispatch
use expression AcceptGrpcSellerOrders as accept
connect api.receiveOrders -> accept()
connect dispatch.takeOrders -> accept() schedule every 43s
# @ocean-meta-start
# tags:
# - service
# - seller
# - partner
# - scheduling
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: rest-seller
# @ocean-meta-end
@service
# Receives orders both ways: OrderDispatch pushes to its API on a timer, and it
# polls DispatchRestApi on its own timer.
RestSeller
use config CommonServiceConfig as cfg
impl api RestSellerApi as api on cfg.apiConfig.port
use api DispatchRestApi as dispatch
use expression AcceptRestSellerOrders as accept
connect api.receiveOrders -> accept()
connect dispatch.takeOrders -> accept() schedule every 33s
# @ocean-meta-start
# tags:
# - service
# - seller
# - partner
# - scheduling
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: soap-seller
# @ocean-meta-end
@service
# Receives orders both ways: OrderDispatch pushes to its API on a timer, and it
# polls DispatchSoapApi on its own timer.
SoapSeller
use config CommonServiceConfig as cfg
impl api SoapSellerApi as api on cfg.soapApiConfig.port
use api DispatchSoapApi as dispatch
use expression AcceptSoapSellerOrders as accept
connect api.receiveOrders -> accept()
connect dispatch.takeOrders -> accept() schedule every 38s
# @ocean-meta-start
# tags:
# - dashboard
# - operations-monitoring
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-exchange-ui
# @ocean-meta-end
@dashboard
DispatchedDashboard
title: Dispatched
layout: SingleColumnLayout
Widget DispatchedGuide of type Text
title = Orders handed on to sellers
subtitle = Newest first, from the buyer that sent each order to the seller that got it
content = <p>An order appears here once OrderDispatch has taken it from its outbox: pushed to its seller, or collected by a seller's poll. Waited is the time between OrderProcessor receiving the order and its dispatch; it includes the wait for the next batch release.</p>
Widget DispatchedOrders of type Table
title = Dispatched orders
datatype = DispatchedRow
columns:
- id
- sourceId
- product
- quantity
- buyer
- seller
- inboundFlow
- outboundFlow
- receivedAt
- dispatchedAt
- waited
sortable = true
limit = 20
# @ocean-meta-start
# tags:
# - dashboard
# - operations-monitoring
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-exchange-ui
# @ocean-meta-end
@dashboard
FlowDashboard
title: Flow Monitor
layout: SingleColumnLayout
Widget FlowGuide of type Text
title = Twelve flows
subtitle = Every API style, inbound and outbound, pushed and pulled
content = <p>Each row is one flow. Inbound flows count the orders they brought in; outbound flows count the orders they have dispatched. Outbound counts grow once a minute, when OrderDispatch releases the processed batch.</p>
Widget Flows of type Table
title = Flows
datatype = FlowRow
columns:
- flow
- style
- mode
- startedBy
- schedule
- orders
sortable = true
limit = 12
# @ocean-meta-start
# tags:
# - dashboard
# - operations-monitoring
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-exchange-ui
# @ocean-meta-end
@dashboard
OrderDashboard
title: Orders
layout: SingleColumnLayout
Widget OrderGuide of type Text
title = Orders through the exchange
subtitle = Newest first, with the flow each order came in and went out on
content = <p>An order is processed when OrderProcessor receives it, released when OrderDispatch takes the batch, and dispatched once its seller has it.</p>
Widget Orders of type Table
title = Orders
datatype = OrderRow
columns:
- id
- sourceId
- product
- quantity
- buyer
- seller
- inboundFlow
- outboundFlow
- status
- receivedAt
- dispatchedAt
sortable = true
limit = 20
# @ocean-meta-start
# tags:
# - dashboard
# - search
# - operations-monitoring
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-exchange-ui
# @ocean-meta-end
@dashboard
SearchDashboard
title: Search
layout: SingleColumnLayout
Widget SearchGuide of type Text
title = Find an order, or every order for a buyer or seller
subtitle = Look an order up by either of its ids, or follow a buyer's or seller's orders
content = <p>Enter an order id (the exchange's own ORD- id or the buyer's own order id), a buyer, a seller, or any combination: RestBuyer, SoapBuyer, or GrpcBuyer, and RestSeller, SoapSeller, or GrpcSeller. Leave a field empty to match anything; letter case does not matter. Each result shows both ids, the order's flows, its status, and when it was received and dispatched.</p>
Widget OrderSearchForm of type Form
title = Search orders
datatype = OrderQuery
buttons:
submit : π Search orders
cancel : Clear
# @ocean-meta-start
# tags:
# - ui
# - operations-monitoring
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: order-exchange-ui
# @ocean-meta-end
@ui
OrderExchangeUi
@perspectives: version:1.0.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav Flows dashboard = FlowDashboard
nav Orders dashboard = OrderDashboard
nav Dispatched dashboard = DispatchedDashboard
nav Search dashboard = SearchDashboard
header title = Order Exchange
header subtitle = REST, SOAP, and gRPC partners, inbound and outbound, push and pull
header align = center
footer title = Built with Ocean-lab
footer subtitle = Integration example
footer align = center
use dashboard FlowDashboard as flows
use dashboard OrderDashboard as orders
use dashboard DispatchedDashboard as dispatched
use dashboard SearchDashboard as search
use api OrderProcessorApi
connect flows.Flows.listRows -> OrderProcessorApi.listFlows
connect orders.Orders.listRows -> OrderProcessorApi.listOrderRows
connect dispatched.DispatchedOrders.listRows -> OrderProcessorApi.listDispatchedRows
connect search.OrderSearchForm.submit -> OrderProcessorApi.searchOrders
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: order-exchange
# boundedContext: order-exchange
# service: shared
# @ocean-meta-end
@deploy
Name: OrderExchangeDeploy
@import service P.broker.nats.docker@1.0.0 as NATS
# Buyers
RestBuyerDeploy
service RestBuyer
replica 1
export 9181:RestBuyer.api
SoapBuyerDeploy
service SoapBuyer
replica 1
export 9182:SoapBuyer.api
GrpcBuyerDeploy
service GrpcBuyer
replica 1
export 9183:GrpcBuyer.api
# Exchange
OrderIntakeDeploy
service OrderIntakeService
replica 1
export 9101:OrderIntakeService.restIn
export 9102:OrderIntakeService.soapIn
export 9103:OrderIntakeService.grpcIn
dependsOn Broker, RestBuyerDeploy, SoapBuyerDeploy, GrpcBuyerDeploy
OrderProcessorDeploy
service OrderProcessor
replica 1
export 9110:OrderProcessor.api
dependsOn Broker
OrderDispatchDeploy
service OrderDispatchService
replica 1
export 9121:OrderDispatchService.restOut
export 9122:OrderDispatchService.soapOut
export 9123:OrderDispatchService.grpcOut
dependsOn OrderProcessorDeploy, RestSellerDeploy, SoapSellerDeploy, GrpcSellerDeploy
OrderExchangeBrokerDeploy
service OrderExchangeBroker
replica 1
dependsOn Broker
Broker
service NATS
# Sellers
RestSellerDeploy
service RestSeller
replica 1
export 9191:RestSeller.api
SoapSellerDeploy
service SoapSeller
replica 1
export 9192:SoapSeller.api
GrpcSellerDeploy
service GrpcSeller
replica 1
export 9193:GrpcSeller.api
# Monitoring
OrderExchangeUiDeploy
service OrderExchangeUi
replica 1
export 8086:OrderExchangeUi.config
dependsOn OrderProcessorDeploy
flowchart TB
rb(["RestBuyer"])
sb(["SoapBuyer"])
gb(["GrpcBuyer"])
in[["OrderIntake<br/><i>integration Β· in</i>"]]
q@{ shape: h-cyl, label: "orders.inbound<br/><i>broker topic</i>" }
p("OrderProcessor<br/><i>service</i>")
out[["OrderDispatch<br/><i>integration Β· out</i>"]]
rs(["RestSeller"])
ss(["SoapSeller"])
gs(["GrpcSeller"])
%% Solid: push. Dotted: pull. Each label names the party whose timer starts the call.
rb -->|"<b>REST push</b> Β· every 30s<br/>started by RestBuyer"| in
rb -.->|"<b>REST pull</b> Β· every 31s<br/>started by OrderIntake"| in
sb -->|"<b>SOAP push</b> Β· every 35s<br/>started by SoapBuyer"| in
sb -.->|"<b>SOAP pull</b> Β· every 36s<br/>started by OrderIntake"| in
gb -->|"<b>gRPC push</b> Β· every 40s<br/>started by GrpcBuyer"| in
gb -.->|"<b>gRPC pull</b> Β· every 41s<br/>started by OrderIntake"| in
in -->|"<b>publish</b> Β· each order"| q
q -->|"<b>subscribe</b> Β· each order"| p
p -.->|"<b>batch pull</b> Β· every 60s<br/>started by OrderDispatch"| out
out -->|"<b>REST push</b> Β· every 32s<br/>started by OrderDispatch"| rs
out -.->|"<b>REST pull</b> Β· every 33s<br/>started by RestSeller"| rs
out -->|"<b>SOAP push</b> Β· every 37s<br/>started by OrderDispatch"| ss
out -.->|"<b>SOAP pull</b> Β· every 38s<br/>started by SoapSeller"| ss
out -->|"<b>gRPC push</b> Β· every 42s<br/>started by OrderDispatch"| gs
out -.->|"<b>gRPC pull</b> Β· every 43s<br/>started by GrpcSeller"| gs
%% One color per API style, as in the editor: REST green, SOAP pink, gRPC indigo.
linkStyle 0,1,9,10 stroke:#10b981,stroke-width:2px
linkStyle 2,3,11,12 stroke:#ec4899,stroke-width:2px
linkStyle 4,5,13,14 stroke:#6366f1,stroke-width:2px
%% Ocean system parts get a teal border; buyers and sellers are external partners.
classDef partner stroke:#64748b,stroke-width:1px
classDef system stroke:#0f766e,stroke-width:2.5px
class rb,sb,gb,rs,ss,gs partner
class in,q,p,out system
package expression
import (
cryptorand "crypto/rand"
"fmt"
"log"
"math/rand/v2"
"sort"
"strings"
"time"
)
// The six inbound flows are named by their channel ("REST push", ...); the
// same names identify the six outbound flows.
const (
statusProcessed = "processed"
statusReleased = "released"
statusDispatched = "dispatched"
)
// flow describes one of the twelve flows for the Flow Monitor.
type flow struct {
name string
direction string
style string
mode string
startedBy string
schedule string
}
var inboundFlows = []flow{
{"REST push", "in", "REST", "push", "RestBuyer", "every 30s"},
{"REST pull", "in", "REST", "pull", "OrderIntake", "every 31s"},
{"SOAP push", "in", "SOAP", "push", "SoapBuyer", "every 35s"},
{"SOAP pull", "in", "SOAP", "pull", "OrderIntake", "every 36s"},
{"gRPC push", "in", "gRPC", "push", "GrpcBuyer", "every 40s"},
{"gRPC pull", "in", "gRPC", "pull", "OrderIntake", "every 41s"},
}
var outboundFlows = []flow{
{"REST push", "out", "REST", "push", "OrderDispatch", "every 32s"},
{"REST pull", "out", "REST", "pull", "RestSeller", "every 33s"},
{"SOAP push", "out", "SOAP", "push", "OrderDispatch", "every 37s"},
{"SOAP pull", "out", "SOAP", "pull", "SoapSeller", "every 38s"},
{"gRPC push", "out", "gRPC", "push", "OrderDispatch", "every 42s"},
{"gRPC pull", "out", "gRPC", "pull", "GrpcSeller", "every 43s"},
}
var products = []string{
"Industrial Fasteners",
"Office Supplies",
"Workshop Tools",
"Safety Equipment",
"Packaging Materials",
}
func randomProduct() string { return products[rand.IntN(len(products))] }
func randomQuantity() int { return rand.IntN(50) + 1 }
// ------------------------------------------------------------------
// Buyers: each builds an order in its own format.
// ------------------------------------------------------------------
func NewRestBuyerOrder(channel string) (RestBuyerOrder, error) {
id, err := newPrefixedID("RB")
if err != nil {
return RestBuyerOrder{}, err
}
return RestBuyerOrder{OrderId: id, Product: randomProduct(), Quantity: randomQuantity(), Buyer: "RestBuyer", Channel: channel}, nil
}
func NewSoapBuyerOrder(channel string) (SoapBuyerOrder, error) {
id, err := newPrefixedID("SB")
if err != nil {
return SoapBuyerOrder{}, err
}
return SoapBuyerOrder{Reference: id, Item: randomProduct(), Units: randomQuantity(), Party: "SoapBuyer", Channel: channel}, nil
}
func NewGrpcBuyerOrder(channel string) (GrpcBuyerOrder, error) {
id, err := newPrefixedID("GB")
if err != nil {
return GrpcBuyerOrder{}, err
}
return GrpcBuyerOrder{
Code: id,
Line: GrpcBuyerLine{Sku: randomProduct(), Count: randomQuantity()},
Account: "GrpcBuyer",
Channel: channel,
}, nil
}
// ------------------------------------------------------------------
// OrderIntake mappers: buyer format to the canonical Order. The buyer's
// own identifier is replaced by an exchange-wide ORD identifier.
// ------------------------------------------------------------------
func newOrder(sourceId, product string, quantity int, buyer, inboundFlow string) (Order, error) {
id, err := newPrefixedID("ORD")
if err != nil {
return Order{}, err
}
return Order{Id: id, SourceId: sourceId, Product: product, Quantity: quantity, Buyer: buyer, InboundFlow: inboundFlow}, nil
}
func MapRestBuyerOrder(source RestBuyerOrder) (Order, error) {
return newOrder(source.OrderId, source.Product, source.Quantity, source.Buyer, source.Channel)
}
func MapSoapBuyerOrder(source SoapBuyerOrder) (Order, error) {
return newOrder(source.Reference, source.Item, source.Units, source.Party, source.Channel)
}
func MapGrpcBuyerOrder(source GrpcBuyerOrder) (Order, error) {
return newOrder(source.Code, source.Line.Sku, source.Line.Count, source.Account, source.Channel)
}
// ------------------------------------------------------------------
// OrderDispatch mappers: the collected outbox batches to each seller's
// format. An aggregate collects one batch; the mappers accept any number.
// ------------------------------------------------------------------
func ordersOf(batches []OrderBatch) []Order {
orders := make([]Order, 0)
for _, batch := range batches {
orders = append(orders, batch.Orders...)
}
return orders
}
func MapToRestSellerOrders(batches []OrderBatch) ([]RestSellerOrder, error) {
orders := make([]RestSellerOrder, 0)
for _, order := range ordersOf(batches) {
orders = append(orders, RestSellerOrder{SaleId: order.Id, Article: order.Product, Amount: order.Quantity, Customer: order.Buyer, Origin: order.InboundFlow})
}
return orders, nil
}
func MapToSoapSellerOrders(batches []OrderBatch) ([]SoapSellerOrder, error) {
orders := make([]SoapSellerOrder, 0)
for _, order := range ordersOf(batches) {
orders = append(orders, SoapSellerOrder{
Ticket: order.Id,
Line: SoapSellerLine{Goods: order.Product, Pieces: order.Quantity},
Client: order.Buyer,
Origin: order.InboundFlow,
})
}
return orders, nil
}
func MapToGrpcSellerOrders(batches []OrderBatch) ([]GrpcSellerOrder, error) {
orders := make([]GrpcSellerOrder, 0)
for _, order := range ordersOf(batches) {
orders = append(orders, GrpcSellerOrder{Uid: order.Id, CatalogNumber: order.Product, Qty: order.Quantity, Purchaser: order.Buyer, Origin: order.InboundFlow})
}
return orders, nil
}
// ------------------------------------------------------------------
// OrderProcessor helpers. They return new lists rather than modifying
// their input, and OrderProcessor assigns the result to its context.
// ------------------------------------------------------------------
// AcceptOrder keeps an incoming order and assigns its outbound flow, taking
// turns across the six outbound flows so that every seller flow gets orders.
func AcceptOrder(orders []Order, order Order) ([]Order, error) {
order.OutboundFlow = outboundFlows[len(orders)%len(outboundFlows)].name
order.Status = statusProcessed
order.ReceivedAt = time.Now().UTC()
return append(append(make([]Order, 0, len(orders)+1), orders...), order), nil
}
func CountOrdersWithStatus(orders []Order, status string) (int, error) {
count := 0
for _, order := range orders {
if order.Status == status {
count++
}
}
return count, nil
}
// ReleaseProcessedOrders moves every processed order into its outbound
// flow's outbox, which is the set of released orders of that flow.
func ReleaseProcessedOrders(orders []Order) ([]Order, error) {
result := make([]Order, len(orders))
for i, order := range orders {
if order.Status == statusProcessed {
order.Status = statusReleased
}
result[i] = order
}
return result, nil
}
// ReleasedOrdersOf returns the outbox of one outbound flow.
func ReleasedOrdersOf(orders []Order, flow string) ([]Order, error) {
result := make([]Order, 0)
for _, order := range orders {
if order.Status == statusReleased && order.OutboundFlow == flow {
result = append(result, order)
}
}
return result, nil
}
// MarkDispatched empties one outbound flow's outbox once its orders are
// taken, and records when; call it after ReleasedOrdersOf with the same flow.
func MarkDispatched(orders []Order, flow string) ([]Order, error) {
now := time.Now().UTC()
result := make([]Order, len(orders))
for i, order := range orders {
if order.Status == statusReleased && order.OutboundFlow == flow {
order.Status = statusDispatched
order.DispatchedAt = &now
}
result[i] = order
}
return result, nil
}
// ToFlowRows reports one page of the twelve flows: inbound flows count the
// orders they brought in, outbound flows the orders they have dispatched.
// The page carries the true total (here, always twelve) alongside its rows,
// so the UI knows when it has reached the last page.
func ToFlowRows(orders []Order, offset, limit int) (PageFlowRow, error) {
rows := make([]FlowRow, 0, len(inboundFlows)+len(outboundFlows))
for _, f := range inboundFlows {
count := 0
for _, order := range orders {
if order.InboundFlow == f.name {
count++
}
}
rows = append(rows, f.row(count))
}
for _, f := range outboundFlows {
count := 0
for _, order := range orders {
if order.OutboundFlow == f.name && order.Status == statusDispatched {
count++
}
}
rows = append(rows, f.row(count))
}
start, end := pageBounds(len(rows), offset, limit)
return PageFlowRow{Items: rows[start:end], Total: len(rows)}, nil
}
func (f flow) row(count int) FlowRow {
name := f.direction + " Β· " + f.name
return FlowRow{
Id: name,
Flow: name,
Direction: f.direction,
Style: f.style,
Mode: f.mode,
StartedBy: f.startedBy,
Schedule: f.schedule,
Orders: count,
}
}
// ToOrderRows returns one page of orders, newest first, alongside the true
// total order count.
func ToOrderRows(orders []Order, offset, limit int) (PageOrderRow, error) {
start, end := pageBounds(len(orders), offset, limit)
rows := make([]OrderRow, 0, end-start)
for i := start; i < end; i++ {
rows = append(rows, orderRow(orders[len(orders)-1-i]))
}
return PageOrderRow{Items: rows, Total: len(orders)}, nil
}
// FindOrders returns every order, newest first, whose id, buyer, and seller
// match the query. An empty field matches anything; matching ignores case.
// id matches either the exchange's own ORD- id or the buyer's own order id.
func FindOrders(orders []Order, query OrderQuery) ([]OrderRow, error) {
id := strings.TrimSpace(query.Id)
buyer := strings.TrimSpace(query.Buyer)
seller := strings.TrimSpace(query.Seller)
rows := make([]OrderRow, 0)
for i := len(orders) - 1; i >= 0; i-- {
order := orders[i]
if id != "" && !strings.EqualFold(order.Id, id) && !strings.EqualFold(order.SourceId, id) {
continue
}
if buyer != "" && !strings.EqualFold(order.Buyer, buyer) {
continue
}
if seller != "" && !strings.EqualFold(sellerOf(order.OutboundFlow), seller) {
continue
}
rows = append(rows, orderRow(order))
}
return rows, nil
}
// orderRow is one order's journey, as listed and searched. The seller is the
// one its outbound flow leads to; the dispatch time stays empty until the
// order is dispatched.
func orderRow(order Order) OrderRow {
row := OrderRow{
Id: order.Id,
SourceId: order.SourceId,
Product: order.Product,
Quantity: order.Quantity,
Buyer: order.Buyer,
Seller: sellerOf(order.OutboundFlow),
InboundFlow: order.InboundFlow,
OutboundFlow: order.OutboundFlow,
Status: order.Status,
ReceivedAt: formatTime(order.ReceivedAt),
}
if order.DispatchedAt != nil {
row.DispatchedAt = formatTime(*order.DispatchedAt)
}
return row
}
// ToDispatchedRows returns one page of dispatched orders, most recently
// dispatched first, each with the seller that got it and how long it waited,
// alongside the true count of dispatched orders.
func ToDispatchedRows(orders []Order, offset, limit int) (PageDispatchedRow, error) {
dispatched := make([]Order, 0)
for _, order := range orders {
if order.Status == statusDispatched && order.DispatchedAt != nil {
dispatched = append(dispatched, order)
}
}
// Orders are kept in arrival order; sort the dispatched ones newest first.
sort.SliceStable(dispatched, func(i, j int) bool {
return dispatched[i].DispatchedAt.After(*dispatched[j].DispatchedAt)
})
start, end := pageBounds(len(dispatched), offset, limit)
rows := make([]DispatchedRow, 0, end-start)
for _, order := range dispatched[start:end] {
rows = append(rows, DispatchedRow{
Id: order.Id,
SourceId: order.SourceId,
Product: order.Product,
Quantity: order.Quantity,
Buyer: order.Buyer,
Seller: sellerOf(order.OutboundFlow),
InboundFlow: order.InboundFlow,
OutboundFlow: order.OutboundFlow,
ReceivedAt: formatTime(order.ReceivedAt),
DispatchedAt: formatTime(*order.DispatchedAt),
Waited: order.DispatchedAt.Sub(order.ReceivedAt).Round(time.Second).String(),
})
}
return PageDispatchedRow{Items: rows, Total: len(dispatched)}, nil
}
// sellerOf names the seller of an outbound flow: "SOAP pull" goes to SoapSeller.
func sellerOf(flow string) string {
style, _, _ := strings.Cut(flow, " ")
switch style {
case "REST":
return "RestSeller"
case "SOAP":
return "SoapSeller"
case "gRPC":
return "GrpcSeller"
}
return ""
}
func formatTime(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
}
// ------------------------------------------------------------------
// Sellers: log each delivery, pushed or pulled.
// ------------------------------------------------------------------
func AcceptRestSellerOrders(orders []RestSellerOrder) error {
log.Printf("RestSeller received %d order(s)", len(orders))
return nil
}
func AcceptSoapSellerOrders(orders []SoapSellerOrder) error {
log.Printf("SoapSeller received %d order(s)", len(orders))
return nil
}
func AcceptGrpcSellerOrders(orders []GrpcSellerOrder) error {
log.Printf("GrpcSeller received %d order(s)", len(orders))
return nil
}
// ------------------------------------------------------------------
// Shared helpers, as in the purchase-order example.
// ------------------------------------------------------------------
// pageBounds normalizes pagination inputs and returns a safe half-open range.
// A non-positive limit returns the remaining records.
func pageBounds(total, offset, limit int) (int, int) {
if offset < 0 {
offset = 0
}
if offset >= total {
return total, total
}
if limit <= 0 || limit > total-offset {
limit = total - offset
}
return offset, offset + limit
}
// newPrefixedID produces IDs compatible with the DSL's <prefix>-UUID pattern
// without a dependency on a third-party UUID package.
func newPrefixedID(prefix string) (string, error) {
bytes := make([]byte, 16)
if _, err := cryptorand.Read(bytes); err != nil {
return "", fmt.Errorf("generate %s identifier: %w", prefix, err)
}
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
return fmt.Sprintf("%s-%08x-%04x-%04x-%04x-%012x",
prefix,
bytes[0:4],
bytes[4:6],
bytes[6:8],
bytes[8:10],
bytes[10:16],
), nil
}
flowchart TB
rb(["RestBuyer"])
sb(["SoapBuyer"])
gb(["GrpcBuyer"])
rbApi[/"RestBuyerApi"/]
iRest[/"IntakeRestApi"/]
sbApi[/"SoapBuyerApi"/]
iSoap[/"IntakeSoapApi"/]
gbApi[/"GrpcBuyerApi"/]
iGrpc[/"IntakeGrpcApi"/]
intake("OrderIntakeService<br/><i>hosts OrderIntake</i>")
q@{ shape: h-cyl, label: "orders.inbound<br/><i>broker topic</i>" }
proc("OrderProcessor")
procApi[/"OrderProcessorApi"/]
ui("OrderExchangeUi")
dispatch("OrderDispatchService<br/><i>hosts OrderDispatch</i>")
rsApi[/"RestSellerApi"/]
dRest[/"DispatchRestApi"/]
ssApi[/"SoapSellerApi"/]
dSoap[/"DispatchSoapApi"/]
gsApi[/"GrpcSellerApi"/]
dGrpc[/"DispatchGrpcApi"/]
rs(["RestSeller"])
ss(["SoapSeller"])
gs(["GrpcSeller"])
%% Arrows: the direction orders move. Solid: push, dotted: pull.
%% Thick lines: an API and the service that implements it.
rb -->|"push Β· submitOrder"| iRest
iRest === intake
rb === rbApi
rbApi -.->|"pull Β· nextOrder"| intake
sb -->|"push Β· submitOrder"| iSoap
iSoap === intake
sb === sbApi
sbApi -.->|"pull Β· nextOrder"| intake
gb -->|"push Β· submitOrder"| iGrpc
iGrpc === intake
gb === gbApi
gbApi -.->|"pull Β· nextOrder"| intake
intake -->|"publish Β· each order"| q
q -->|"subscribe Β· each order"| proc
proc === procApi
procApi -.->|"pull Β· releaseBatch<br/>and takeβ¦Orders"| dispatch
procApi -.->|"pull · list⦠and<br/>searchOrders"| ui
dispatch -->|"push Β· receiveOrders"| rsApi
rsApi === rs
dispatch === dRest
dRest -.->|"pull Β· takeOrders"| rs
dispatch -->|"push Β· receiveOrders"| ssApi
ssApi === ss
dispatch === dSoap
dSoap -.->|"pull Β· takeOrders"| ss
dispatch -->|"push Β· receiveOrders"| gsApi
gsApi === gs
dispatch === dGrpc
dGrpc -.->|"pull Β· takeOrders"| gs
%% One color per API style, as in the editor: REST green, SOAP pink, gRPC indigo.
linkStyle 0,3,17,20 stroke:#10b981,stroke-width:1.5px
linkStyle 1,2,18,19 stroke:#10b981,stroke-width:3.5px
linkStyle 4,7,21,24 stroke:#ec4899,stroke-width:1.5px
linkStyle 5,6,22,23 stroke:#ec4899,stroke-width:3.5px
linkStyle 8,11,25,28 stroke:#6366f1,stroke-width:1.5px
linkStyle 9,10,26,27 stroke:#6366f1,stroke-width:3.5px
linkStyle 14 stroke-width:3.5px
classDef partner stroke:#64748b,stroke-width:1px
classDef system stroke:#0f766e,stroke-width:2.5px
classDef rest stroke:#10b981,stroke-width:2px
classDef soap stroke:#ec4899,stroke-width:2px
classDef grpc stroke:#6366f1,stroke-width:2px
class rb,sb,gb,rs,ss,gs partner
class intake,q,proc,dispatch,ui system
class rbApi,iRest,procApi,rsApi,dRest rest
class sbApi,iSoap,ssApi,dSoap soap
class gbApi,iGrpc,gsApi,dGrpc grpc