Order Management: Entity-Based FSM
Persist orders and let a finite state machine enforce their approval, shipment, delivery, and cancellation lifecycle.
Examplefsmfinite-state-machineentity-basedentitystatetransitionordersdatabaseworkflowexpressionsapiui
π Horizon
Order Lifecycle at a Glance
Learning Scenario
Every order has its own lifecycle. A new order may be approved or cancelled; an approved order may be shipped; and a shipped order may be delivered. Actions that do not make sense in the current state should not be available as valid transitions.
An entity-based FSM expresses these rules next to the entity it controls. Each order is identified independently, and its current status remains part of the persisted order rather than process-local memory.
Lifecycle
What It Demonstrates
- An FSM controlling the status field of an identified entity.
- Different valid events in different lifecycle states.
- Updating entity data as part of a transition.
- Lifecycle commands alongside creation, deletion, and queries.
- Initializing status and timestamps before persistence.
Expected Result
You will be able to create and find orders, move each order through its valid lifecycle, and observe its updated status and timestamp after every successful transition.
π§ Voyage
1. Model Orders and Their States
Start by naming every state an order may occupy. Using an enum prevents arbitrary strings from becoming lifecycle states.
@datatype
enum OrderStatus
new
approved
shipped
delivered
cancelled
The FSM will control the status field. The patterned ID is
equally important because it identifies which order should transition.
Order
id pattern O-UUID
name : String
status : OrderStatus
customerId: String
total : Float
createdAt : DateTime
updatedAt : DateTime
2. Make Order a Persistent Entity
Declaring Order as an entity gives each FSM instance a
persistent record. The entity key will match the FSM key used later.
@database
Database OrderDb
engine = postgres
configType = DatabaseConfig
Entity Order
key(id)
indexes: index(status), index(customerId), index(createdAt)
Queries support lookup, pagination, and searches. Commands cover normal persistence operations; lifecycle updates will be owned by the FSM.
query findById(id:String) : Order
query listOrders(offset:Int, limit:Int) : List<Order>
query findByStatus(status:OrderStatus) : List<Order>
query findByCustomerId(customerId:String) : List<Order>
command T createOrder(order:Order) : Order
command T updateOrder(order:Order) : Order
command deleteOrder(id:String) : _
3. Separate Queries from Lifecycle Commands
Read endpoints expose the entity without changing its state. Callers can retrieve an order, page through orders, or search by useful fields.
get /order/{id} getOrder(id:String) : Order
get /orders/{offset}/{limit} listOrders(offset:Int, limit:Int) : List<Order>
get /order/find/by-status/{status} findByStatus(status:OrderStatus) : List<Order>
Approve, ship, deliver, and cancel are explicit business commands. Each receives only an ID; the FSM uses it to find the entity and its state.
post /order createOrder(order:Order) : Order
post /order/{id}/approve approveOrder(id:String) : Order
post /order/{id}/ship shipOrder(id:String) : Order
post /order/{id}/deliver deliverOrder(id:String) : Order
post /order/{id}/cancel cancelOrder(id:String) : Order
post /order/{id}/delete deleteOrder(id:String) : _
4. Initialize Every New Order Consistently
Callers should not choose initial lifecycle data. This expression sets both timestamps and forces every new order into the expected state.
AddNewOrderWithTimestamp
input: order:Order
output: result:Order
logic
var ts DateTime
ts = nowUTC()
order.createdAt = ts
order.updatedAt = ts
order.status = OrderStatus.new
result = Order.createOrder(order)
5. Bind the FSM to the Entity
The first line contains the key idea of an entity-based FSM:
@fsm
OrderFSM controls Order.status
key: id
OrderFSMnames the machine.controls Order.statusbinds state to the entity field.key: ididentifies the particular order being changed.
Order A can therefore be New while Order B is Shipped. Each lifecycle state belongs toβand remains stored withβits own entity.
Declare typed lifecycle events
The ID is implicit because it is the FSM key. No other event input is needed, and every event returns the updated order.
event approve in(_) out(order:Order)
event ship in(_) out(order:Order)
event deliver in(_) out(order:Order)
event cancel in(_) out(order:Order)
See the complete lifecycle
6. Define Valid Transitions State by State
A New order accepts only approve and cancel.
next changes the controlled status, while this
refers to the order currently handled by the FSM.
state New
on event approve:
next Approved: this.updatedAt = nowUTC()
on event cancel:
next Cancelled: this.updatedAt = nowUTC()
The transition changes status and refreshes updatedAt
together. The event returns the resulting entity declared by its output.
Continue the lifecycle
state Approved
on event ship:
next Shipped: this.updatedAt = nowUTC()
on event cancel:
next Cancelled: this.updatedAt = nowUTC()
state Shipped
on event deliver:
next Delivered: this.updatedAt = nowUTC()
Missing handlers express business rules. New cannot be shipped, and a Shipped order cannot be approved or cancelled in this model.
Mark terminal states
state Delivered
state Cancelled
Empty bodies are intentional: these are valid persisted states with no outgoing lifecycle events.
7. Wire Persistence and Lifecycle Operations
The service brings the API, database, FSM, and creation expression together without duplicating their responsibilities.
OrderMgtService
impl api OrderApi as api on myCfg.apiConfig.port
use database OrderDb as odb
use fsm OrderFSM as oFsm
use expression AddNewOrderWithTimestamp as addOrder
Creation and queries connect to expressions or entity operations:
connect api.createOrder -> addOrder
connect api.getOrder -> Order.findById
connect api.listOrders -> Order.listOrders
connect api.deleteOrder -> Order.deleteOrder
connect api.findByStatus -> Order.findByStatus
Lifecycle commands must go through the FSM so status cannot bypass its transition rules.
connect api.approveOrder -> oFsm.approve
connect api.shipOrder -> oFsm.ship
connect api.deliverOrder -> oFsm.deliver
connect api.cancelOrder -> oFsm.cancel
8. Configure the API and Persistence
The backend needs an API port, database connection, and logging. Keeping these in config types avoids embedding environment details in the model.
@config
@import config O.database.postgres.config@1.0.0 as DatabaseConfig
@import config O.log.config@1.0.0 as LogConfig
OrderMgtConfig
apiConfig: ApiConfig
dbConfig: DatabaseConfig
logConfig: LogConfig
9. Build a UI Around the Lifecycle
Separate dashboards cover viewing, creating, searching, and managing orders. Every management form asks for the FSM key: the order ID.
connect view.OrderTable.listRows -> OrderApi.listOrders
connect order.CreateForm.submit -> OrderApi.createOrder
connect manage.ApproveForm.submit -> OrderApi.approveOrder
connect manage.ShipForm.submit -> OrderApi.shipOrder
connect manage.DeliverForm.submit -> OrderApi.deliverOrder
connect manage.CancelForm.submit -> OrderApi.cancelOrder
The UI never writes status directly. It invokes business commands, and the service routes those commands through the FSM.
10. Deploy the Persistent Workflow
The backend depends on the database and exposes its API on port
9097. The UI is exposed on 8087 and depends on
the backend.
OrderMgtDeploy
service OrderMgtService
replica 1
export 9097:OrderMgtService.api
dependsOn OrderMgtDatabase
OrderMgtDatabase
service PostgresqlDB
OrderMgtUiDeploy
service OrderMgtUi
replica 1
export 8087:OrderMgtUi.config
dependsOn OrderMgtDeploy
Because state is stored in each order entity, lifecycle progress remains available after the backend restarts.
11. Exercise Valid and Invalid Lifecycles
- Validate and generate the complete example.
- Start the database, backend, and UI.
- Open
localhost:8087and create two orders. - Verify that both begin in
new. - Approve, ship, and deliver the first order in sequence.
- Cancel the second order directly from New.
- Search by status and inspect each updated timestamp.
- Try shipping a New order or cancelling a Delivered order.
- Restart the backend and confirm that statuses remain stored.
Advancing one order changes only the entity identified by that ID. All other orders remain in their own independent lifecycle states.
12. Conclusion
The FSM does more than assign a status. It identifies an entity, checks which event is valid in its current state, updates the controlled field and related data, persists the change, and returns the resulting order.
Afterwards, you should understand how to:
- bind an FSM to an entity field with
controls; - identify independent FSM instances using
key; - declare events whose entity ID is implicit;
- define allowed events separately for each state;
- transition with
nextand updatethis; - represent terminal states with empty state bodies;
- route lifecycle commands through the FSM;
- combine persisted workflows with CRUD and search operations.
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: entity-order-fsm
# @ocean-meta-end
@info
name: OrderManagementService
version: 1.0.0
description: OrderManagementService is a simple example service demonstrating
the use of entity-based finite state machines (FSMs) in the Ocean-lab DSL
platform.
It manages the lifecycle of customer orders, modeled with a dedicated
`Order` datatype and an `OrderStatus` enum. The FSM governs allowed state
transitions (e.g. New β Approved β Shipped β Delivered) and ensures that
business rules are enforced consistently.
The service exposes an API for creating, updating, and querying orders.
Internally, it combines DSL concepts such as typed datatypes, lifecycle
management through FSMs, and clean persistence via entity-based design.
OrderManagementService is a foundational example for exploring how Ocean-lab
DSL can express domain logic, enforce valid state transitions, and integrate
APIs, FSMs, and databases in a cohesive manner.
# @ocean-meta-start
# tags:
# - order-management
# - datatype
# perspective:
# feature: entity-order-fsm
# service: order-mgt-service
# @ocean-meta-end
@datatype
enum OrderStatus
new
approved
shipped
delivered
cancelled
Order
id pattern O-UUID
name : String
status : OrderStatus
customerId: String
total : Float
createdAt : DateTime
updatedAt : DateTime
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: entity-order-fsm
# service: all
# @ocean-meta-end
@config
@import config O.database.postgres.config@1.0.0 as DatabaseConfig
@import config O.log.config@1.0.0 as LogConfig
OrderMgtConfig
apiConfig : ApiConfig
dbConfig : DatabaseConfig
logConfig : LogConfig
ApiConfig
port: Int (default=8080)
UiConfig
port: Int (default=8080)
logConfig : LogConfig
# @ocean-meta-start
# tags:
# - order-management
# - database
# perspective:
# feature: entity-order-fsm
# service: order-mgt-service
# @ocean-meta-end
@database
Database OrderDb
engine = postgres
configType = DatabaseConfig
tags = primary
# ---------------------------------------------------
# Order Entity
# ---------------------------------------------------
Entity Order
key(id)
indexes: index(status), index(customerId), index(createdAt)
# Queries
query findById(id: String) : Order
query listAllOrders() : List<Order>
query listOrders(offset: Int, limit: Int) : List<Order>
query findByName(name: String) : List<Order>
query findByStatus(status: OrderStatus) : List<Order>
query findByCustomerId(customerId: String) : List<Order>
# Commands
command T createOrder(order: Order) : Order
command T updateOrder(order: Order) : Order
command deleteOrder(id: String) : _
# @ocean-meta-start
# tags:
# - order-management
# - order-mgt-api
# - rest-api
# perspective:
# feature: entity-order-fsm
# service: order-mgt-service
# @ocean-meta-end
@api
OrderApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
generateSwagger = true
# -------------------------
# Queries
# -------------------------
get /orders listAllOrders() : List<Order>
get /orders/{offset}/{limit} listOrders(offset:Int, limit:Int) : List<Order>
get /order/{id} getOrder(id:String) : Order
get /order/find/by-name/{name} findByName(name:String) : List<Order>
get /order/find/by-status/{status} findByStatus(status:OrderStatus) : List<Order>
get /order/find/by-customer-Id/{customerId} findByCustomerId(customerId:String) : List<Order>
# -------------------------
# Commands
# -------------------------
post /order createOrder(order:Order) : Order
post /order/{id}/approve approveOrder(id:String) : Order
post /order/{id}/ship shipOrder(id:String) : Order
post /order/{id}/deliver deliverOrder(id:String) : Order
post /order/{id}/cancel cancelOrder(id:String) : Order
post /order/{id}/delete deleteOrder(id:String) : _
# @ocean-meta-start
# tags:
# - order-management
# - expression
# perspective:
# feature: entity-order-fsm
# service: order-mgt-service
# @ocean-meta-end
@expression
AddNewOrderWithTimestamp
input : order:Order
output : result:Order
logic
var ts DateTime
ts = nowUTC()
order.createdAt = ts
order.updatedAt = ts
order.status = OrderStatus.new
result = Order.createOrder(order)
# @ocean-meta-start
# tags:
# - order-management
# - entity-fsm
# - fsm
# perspective:
# feature: entity-order-fsm
# service: order-mgt-service
# @ocean-meta-end
@fsm
OrderFSM controls Order.status
key: id
#database: OrderDb
# ---------------------------------------------------
# Events (id is implicit input)
# ---------------------------------------------------
event approve in(_) out(order:Order)
event ship in(_) out(order:Order)
event deliver in(_) out(order:Order)
event cancel in(_) out(order:Order)
# ---------------------------------------------------
# States
# ---------------------------------------------------
state New
on event approve:
next Approved : this.updatedAt = nowUTC()
on event cancel:
next Cancelled : this.updatedAt = nowUTC()
state Approved
on event ship:
next Shipped : this.updatedAt = nowUTC()
on event cancel:
next Cancelled : this.updatedAt = nowUTC()
state Shipped
on event deliver:
next Delivered : this.updatedAt = nowUTC()
state Delivered
state Cancelled
# @ocean-meta-start
# tags:
# - order-management
# - entity-fsm
# - order-mgt-api
# - service
# perspective:
# feature: entity-order-fsm
# service: order-mgt-service
# @ocean-meta-end
@service
OrderMgtService
@perspectives: version:0.1.0, lifestyle:stable
use config OrderMgtConfig as myCfg
impl api OrderApi as api on myCfg.apiConfig.port
use database OrderDb as odb
use fsm OrderFSM as oFsm
use expression AddNewOrderWithTimestamp as addOrder
# -------------------------
# Config binding
# -------------------------
connect myCfg.dbConfig -> odb
# -------------------------
# Expressions
# -------------------------
# (future expressions like CalculateTotal, ValidateOrder, etc.)
# use expression CalculateTotalExpression
# use expression ValidateOrderExpression
# -------------------------
# Order Operations
# -------------------------
connect api.listAllOrders -> Order.listAllOrders
connect api.listOrders -> Order.listOrders
connect api.getOrder -> Order.findById
connect api.createOrder -> addOrder
connect api.deleteOrder -> Order.deleteOrder
connect api.approveOrder -> oFsm.approve
connect api.shipOrder -> oFsm.ship
connect api.deliverOrder -> oFsm.deliver
connect api.cancelOrder -> oFsm.cancel
connect api.findByName -> Order.findByName
connect api.findByStatus -> Order.findByStatus
connect api.findByCustomerId -> Order.findByCustomerId
# @ocean-meta-start
# tags:
# - ui
# - dashboard
# perspective:
# feature: entity-order-fsm
# service: ui-service
# @ocean-meta-end
@dashboard
ViewDashboard
title: Orders
subtitle: See all your orders here!
layout: SingleColumnLayout
Widget OrderTable of type Table
title = Orders Table
limit = 5
columns:
- id String
- name String
- status OrderStatus
- customerId String
- total Float
- createdAt DateTime
- updatedAt DateTime
OrderDashboard
title: Create or Delete Orders!
layout: SingleColumnLayout
Widget CreateForm of type Form
title = Create Order
fields:
- name String
- customerId String
- total Float
buttons:
submit : π₯ Create
cancel : Reset
Widget S1 of type Separator
label = DELETE
size = lg
style = solid
Widget DeleteForm of type Form
title = Delete Order
fields:
- id String
buttons:
submit : β Delete
cancel : Reset
SearchDashboard
title: Find Orders!
layout: SingleColumnLayout
Widget FindByNameForm of type Form
title = Find Order By Name
fields:
- name String
buttons:
submit : π Find by Name
Widget S2 of type Separator
size = lg
style = solid
Widget FindByStatusForm of type Form
title = Find Order By Status
fields:
- status OrderStatus
buttons:
submit : π Find by Status
Widget S3 of type Separator
size = lg
style = solid
Widget FindByCustomerIdForm of type Form
title = Find Order By Customer-ID
fields:
- customerId String
buttons:
submit : π Find by Customer-ID
ManageDashboard
title: Manage Orders!
subtitle: Manage your orders like a boss!
layout: SingleColumnLayout
Widget ApproveForm of type Form
title = Approve Order
fields:
- id String
buttons:
submit : π’ Approve
Widget S3 of type Separator
size = lg
style = solid
Widget ShipForm of type Form
title = Ship Order
fields:
- id String
buttons:
submit : π Ship
Widget S5 of type Separator
size = lg
style = solid
Widget DeliverForm of type Form
title = Deliver Order
fields:
- id String
buttons:
submit : π Deliver
Widget S6 of type Separator
size = lg
style = solid
Widget CancelForm of type Form
title = Cancer Order
fields:
- id String
buttons:
submit : π Cancel
AboutDashboard
title: About
subtitle: App information!
layout: SingleColumnLayout
Widget AboutText of type Text
title = About Order Management App
subtitle = This app helps you to manage your orders π¦
content = Order Management App is a simple example service demonstrating the use of entity-based finite state machines (FSMs) in the Ocean-lab DSL platform. <br><br> It manages the lifecycle of customer orders, modeled with a dedicated `Order` datatype and an `OrderStatus` enum. The FSM governs allowed state transitions (e.g. New β Approved β Shipped β Delivered) and ensures that business rules are enforced consistently.<br><br> The backend service exposes an API for creating, updating, and querying orders. Internally, it combines DSL concepts such as typed datatypes, lifecycle management through FSMs, and clean persistence via entity-based design. <br><br> Order Management App is a foundational example for exploring how Ocean-lab DSL can express domain logic, enforce valid state transitions, and integrate APIs, FSMs, and databases in a cohesive manner.
# @ocean-meta-start
# tags:
# - ui
# perspective:
# feature: entity-order-fsm
# service: ui-service
# @ocean-meta-end
@ui
OrderMgtUi
@perspectives: version:0.1.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav View dashboard = ViewDashboard
nav Order dashboard = OrderDashboard
nav Search dashboard = SearchDashboard
nav Manage dashboard = ManageDashboard
nav About dashboard = AboutDashboard
header title = π¦ Order Management App!
header subtitle = Managing orders made simple π
header align = center
footer title = β‘ Built with Ocean-lab
footer subtitle = Version 1.0.0
footer align = center
use dashboard ViewDashboard as view
use dashboard OrderDashboard as order
use dashboard SearchDashboard as search
use dashboard ManageDashboard as manage
use dashboard AboutDashboard as about
use api OrderApi
connect view.OrderTable.listRows -> OrderApi.listOrders
connect order.CreateForm.submit -> OrderApi.createOrder
connect order.DeleteForm.submit -> OrderApi.deleteOrder
connect manage.ApproveForm.submit -> OrderApi.approveOrder
connect manage.ShipForm.submit -> OrderApi.shipOrder
connect manage.DeliverForm.submit -> OrderApi.deliverOrder
connect manage.CancelForm.submit -> OrderApi.cancelOrder
connect search.FindByNameForm.submit -> OrderApi.findByName
connect search.FindByStatusForm.submit -> OrderApi.findByStatus
connect search.FindByCustomerIdForm.submit -> OrderApi.findByCustomerId
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: entity-order-fsm
# service: all
# @ocean-meta-end
@deploy
Name: OrderMgt-deploy
@import service P.db.postgres.docker@1.0.0 as PostgresqlDB
OrderMgtDeploy
service OrderMgtService
replica 1
export 9097:OrderMgtService.api
dependsOn OrderMgtDatabase
OrderMgtDatabase
service PostgresqlDB
OrderMgtUiDeploy
service OrderMgtUi
replica 1
export 8087:OrderMgtUi.config
dependsOn OrderMgtDeploy
flowchart
u[User]
subgraph sys[System]
subgraph s[OrderManagementService]
a[API]
f[FSM]
e[Expressions]
end
d[(Database)]
end
u -.communicates.-> a
a --commands--> f
a --queries----> d
f --manages--> d
f -.uses.-> e