Skip to main content

Design Patterns

1. Dependency Injection (DI)

Used throughout for loose coupling and testability.

Example — registering and resolving a service:

// boot/services.go — register a service instance
di.ToFetch("myService", NewMyService())

// Anywhere in the application — resolve the service
myService := di.Fetch("myService").(*MyService)
myService.DoWork()

Example — injecting a transition via the DI container:

// modules/di.go
di.DependencyInjection["my_module"] = func(name string) {
di.ToResolve(defines.TransitionPrefix+"my_module/my_transition", func() interface{} {
return workflow.ServiceTransitionMapping{
ServiceName: name,
Name: "my_transition",
Impl: NewMyTransition(),
}
})
}

2. Factory Pattern

Services and transitions are created via factories registered in the DI container.

Example — factory-created transition:

// modules/my_module/transitions/my_transition.go
type MyTransition struct {
workflow.BaseServiceTransitions
}

func NewMyTransition() *MyTransition {
return &MyTransition{}
}

func (t *MyTransition) Execute(ctx *workflow.WorkerSessionContext) domain.FlowStepResult {
return domain.FlowStepResult{
Success: true,
Response: map[string]interface{}{"result": "done"},
StatusCode: 200,
}
}
// modules/di.go — factory registration
di.ToResolve(defines.TransitionPrefix+"my_module/my_transition", func() interface{} {
return workflow.ServiceTransitionMapping{
ServiceName: "my_module",
Name: "my_transition",
Impl: NewMyTransition(), // created by factory each time
}
})

3. State Machine

Workflows are implemented as state machines with transitions.

Example — WSL workflow as a state machine:

module example

import services/common

const {
event: "process_order"
}

workflow order_processing {
start: ValidateOrder

state ValidateOrder {
action orders/validator.Validate(event: $constants.event) as Validation
on success -> ProcessPayment(Validation)
on error -> HandleError
}

state ProcessPayment(Validation) {
action payments/processor.Charge(orderId: $Validation.orderId) as Payment
on success -> Complete(Payment)
on error -> HandleError
}

state Complete(Payment) {
action services/common/response.ResponseValue(statusCode: 200, message: "Order placed")
end ok
}

state HandleError {
action services/common/response.ResponseError(message: "Processing failed", code: 500)
end error
}
}

Example — Go-level state node execution:

// engine/workflow/engine.go — engine iterates states by calling ProcessState
// ProcessState returns (success bool, nextStateName string).
// The loop continues until the current state is terminal (no next state).
for engine.can(flow) {
worker.ProcessState(engine, flow) // advances flow.CurrentState to the next state
}

4. Strategy Pattern

Different transition implementations can be swapped via the module system.

Example — interchangeable notification strategies:

// Strategy interface (implicit via ServiceTransitionMapping)
type NotifyTransition interface {
Notify(ctx *workflow.WorkerSessionContext) domain.FlowStepResult
}

// Strategy A: email notification
type EmailNotifyTransition struct{ workflow.BaseServiceTransitions }

func (t *EmailNotifyTransition) Notify(ctx *workflow.WorkerSessionContext) domain.FlowStepResult {
// send email ...
return domain.FlowStepResult{Success: true, StatusCode: 200}
}

// Strategy B: SMS notification
type SMSNotifyTransition struct{ workflow.BaseServiceTransitions }

func (t *SMSNotifyTransition) Notify(ctx *workflow.WorkerSessionContext) domain.FlowStepResult {
// send SMS ...
return domain.FlowStepResult{Success: true, StatusCode: 200}
}
// modules/di.go — swap strategy by changing the Impl field
di.ToResolve(defines.TransitionPrefix+"notifications/notify", func() interface{} {
return workflow.ServiceTransitionMapping{
ServiceName: "notifications",
Name: "notify",
Impl: NewEmailNotifyTransition(), // swap to NewSMSNotifyTransition() as needed
}
})

5. Builder Pattern

WSL compilation uses builder pattern for AST construction.

Example — AST built incrementally during parsing:

// internal/wsl/build_ast.go — builder accumulates nodes
type ModuleBuilder struct {
module *Module
}

func NewModuleBuilder(name string) *ModuleBuilder {
return &ModuleBuilder{module: &Module{Name: name}}
}

func (b *ModuleBuilder) AddImport(path string) *ModuleBuilder {
b.module.Imports = append(b.module.Imports, Import{Path: path})
return b
}

func (b *ModuleBuilder) AddWorkflow(wf Workflow) *ModuleBuilder {
b.module.Workflows = append(b.module.Workflows, wf)
return b
}

func (b *ModuleBuilder) Build() *Module {
return b.module
}

// Usage during compilation
mod := NewModuleBuilder("example").
AddImport("services/common").
AddWorkflow(parsedWorkflow).
Build()

Example — IR graph constructed from AST:

// internal/wsl/ir.go — graph builder
graph := &Graph{
WorkflowName: wf.Name,
Nodes: make(map[string]*Node),
Start: wf.Start,
Constants: extractConstants(mod),
}

for _, state := range wf.States {
graph.Nodes[state.Name] = buildNode(state)
}

6. Event-Driven Architecture

Components communicate via the event bus for loose coupling.

Example — subscribing to workflow lifecycle events:

// Subscribe before the engine starts (e.g., in boot/services.go)
event.Bus.Subscribe("on:workflow:complete", func(wfConfig domain.WorkflowConfigItem, contexts []map[string]interface{}) {
log.Printf("Workflow '%s' completed with %d context(s)", wfConfig.Name, len(contexts))
// persist audit log, notify external system, etc.
})

event.Bus.Subscribe("on:workflow:exit", func(wfConfig domain.WorkflowConfigItem) {
log.Printf("Workflow '%s' exited", wfConfig.Name)
})

Example — publishing a custom application event:

// Inside a transition implementation
func (t *MyTransition) Execute(ctx *workflow.WorkerSessionContext) domain.FlowStepResult {
result := processData(ctx)
// Notify other components without direct coupling
event.Bus.Publish("custom:data:processed", result)
return domain.FlowStepResult{Success: true, Response: result, StatusCode: 200}
}

// Any other component can react independently
event.Bus.Subscribe("custom:data:processed", func(result map[string]interface{}) {
cache.Invalidate(result["id"].(string))
})

7. Repository Pattern

Domain models provide data access abstraction.

Example — accessing workflow configuration through domain models:

// engine/domain/workflow.go — domain model acts as repository abstraction
type Workflow struct {
Name string
Description string
Source string // path to .wsl or .json file
}

// engine/manager/ — manager layer uses domain model, not raw storage
func (m *WorkflowManager) FindByName(name string) (*domain.Workflow, error) {
for _, wf := range m.workflows {
if wf.Name == name {
return wf, nil
}
}
return nil, fmt.Errorf("workflow %q not found", name)
}

Example — using the repository in a transition:

func (t *WorkflowTransition) Run(ctx *workflow.WorkerSessionContext) domain.FlowStepResult {
wf, err := ctx.WorkflowManager.FindByName(ctx.GetParam("workflowName"))
if err != nil {
return domain.FlowStepResult{Success: false, StatusCode: 404}
}
return domain.FlowStepResult{Success: true, Response: wf, StatusCode: 200}
}

Extension Points

1. Custom Modules

Create new modules in modules/ directory with custom transitions.

2. Custom Workflows

Define workflows in WSL or JSON format in runtime/workflows/.

3. Event Handlers

Subscribe to events for custom behavior.

4. Configuration Profiles

Add environment-specific configurations in runtime/etc/.

5. Custom Services

Register custom services in the DI container via boot/services.go.