Skip to content
Ocean-Atlasv0.1.0Canonical Knowledge

Component DSL Reference

The @component section defines reusable in-process business behavior.

A component sits between an expression and a service:

  • an expression implements focused, stateless logic;
  • a component composes expressions, FSMs, and other components into a reusable typed flow;
  • a service provides the runtime boundary and connects behavior to APIs, brokers, databases, configuration, secrets, and deployment.

A component:

  • exposes typed input and output ports;
  • declares every behavioral dependency explicitly;
  • connects its ports and dependencies through typed connections;
  • may be reused by multiple services or nested inside other components;
  • is compiled into its hosting service and is not deployed independently.

A component does not expose a network endpoint, select a port, own deployment configuration, or directly consume runtime infrastructure. Those responsibilities remain with the hosting service.


Use a component when behavior:

  • consists of more than one expression or stateful element;
  • represents a cohesive domain capability;
  • should be reused by multiple services;
  • benefits from an explicit, inspectable internal flow;
  • does not need an independent process or transport boundary.

Examples include:

  • order acceptance;
  • checkout processing;
  • invoice approval;
  • shipment preparation;
  • reusable fraud-screening or eligibility flows.

Do not use a component merely to rename one expression. Do not use a component for behavior that must be independently deployed, scaled, secured, or exposed over a transport; use a service or integration for that boundary.


@component
<ComponentName>
input : <inputName>:<InputType> [& ...]
output : <outputName>:<OutputType> [& ...]
use expression <ExpressionName> as <alias>
use fsm <FSMName> as <alias>
use component <ComponentName> as <alias>
connect <source> -> <target>
connect <source> -<mapper>-> <target>

A file may define multiple components. Each definition continues until the next component name or the end of the file.

Example:

@component
OrderAcceptance
input : submit:SubmitOrderRequest
output : accepted:OrderAccepted & rejected:OrderRejected
use expression ValidateOrder as validate
use expression ToOrderAccepted as toAccepted
use fsm OrderLifecycle as lifecycle
connect submit -> validate
connect validate -> lifecycle.submit
connect lifecycle.accepted -toAccepted-> accepted
connect lifecycle.rejected -> rejected

Component names follow Ocean type-name conventions: they begin with an uppercase letter and contain only letters, digits, and _.

OrderAcceptance
CheckoutFlow
ShipmentPreparation2

The common @perspectives declaration may follow the component name:

OrderAcceptance
@perspectives: version:1.0.0, lifestyle:stable

File metadata and comments follow the common Ocean metadata and comment rules. Comments attached to a component, dependency, port, or connection remain associated with that item during formatting.


Ports form the complete public contract of a component. A hosting service or parent component can reference only these public ports; internal aliases and connections are encapsulated.

An input port activates component behavior with a typed value:

input : submit:SubmitOrderRequest

Multiple inputs are separated by &:

input : submit:SubmitOrderRequest & cancel:CancelOrderRequest

Each activation creates an independent execution scope. Values produced while handling that activation remain correlated with the same scope, including when the component is used to fulfill a request-response API operation.

An output port emits a typed result or signal:

output : accepted:OrderAccepted

Multiple outputs are separated by &:

output : accepted:OrderAccepted & rejected:OrderRejected

One activation may emit one of several outputs, multiple outputs, or no output, depending on its internal behavior. A hosting request-response flow must wire a terminal output that satisfies the implemented API contract.

  • Port names follow Ocean field-name conventions.
  • Port names are unique across both input and output ports of one component.
  • Port types must resolve to Ocean primitive, enum, or datatype definitions.
  • Optional, collection, and map types follow the common Ocean type system.
  • _ is not a port type. A signal without business data uses an explicit signal datatype.
  • A component declares at least one input or output port.

Components declare their dependencies with use. Dependencies are resolved statically and become part of the component’s transitive generation closure.

use expression <ExpressionName> as <alias>

Example:

use expression ValidateOrder as validate
use expression MapAcceptedOrder as mapAccepted

An expression alias is callable. As a connection target it receives the source value; as a connection source it represents the expression result.

use fsm <FSMName> as <alias>

Example:

use fsm OrderLifecycle as lifecycle

FSM events are referenced as <alias>.<event>. As a target the reference sends the value to the event; as a source it represents that event’s declared output. State remains owned by the FSM, not by the component.

A component’s own connections never draw an edge between two events of the same FSM alias — each event is its own independent connection source or target. The canonical idiom (§3, §10, §14) is nonetheless one “trigger” event with no declared output (e.g. submit) driving the FSM, and one or more “outcome” events with no declared input (e.g. accepted, rejected) reading back whichever state it moved to. Every event of one use fsm alias is therefore treated as reachable from every other event of that same alias: once a component’s internal flow reaches any event of an alias, every other event of that alias that feeds one of the component’s own connections becomes reachable too, called with no incoming value. Calling an outcome event while the FSM is not in the state that handles it is always well-defined — the FSM contract requires every unimplemented state/event pair to return its declared zero values, never an error — so this reflects an already-guaranteed safe call, not a new failure mode.

use component <ComponentName> as <alias>

Example:

use component AuditTrail as audit

Nested ports are referenced as <alias>.<port>. A nested input is a connection target and a nested output is a connection source.

  • Every component dependency requires an explicit alias.
  • Aliases follow Ocean field-name conventions.
  • Aliases are unique within the component.
  • An alias must not conflict with a public port or another local symbol.
  • Internal references use aliases consistently; original names do not remain alternate local references after aliasing.

Explicit aliases make nested flows stable when reusable definitions are renamed or imported under repository aliases.

The parser enforces this for all three dependency kinds: use expression, use fsm, and use component each require as <alias> and reject the line otherwise. Alias uniqueness within a component, and conflicts with a public port, are not yet validated (§12).


Connections declare component execution and data flow.

connect <source> -> <target>

The source produces a value or signal. The target consumes it.

connect submit -> validate
connect validate -> lifecycle.submit
connect lifecycle.accepted -> accepted
Reference As source As target
Component input port Activating input value Invalid
Component output port Invalid Emitted public output
Expression alias Expression result Expression invocation
<fsm>.<event> Event output Event invocation
<component>.<input> Invalid Nested component activation
<component>.<output> Nested component output Invalid

Direction is determined by the declared contract, not merely by where a name appears. Reversing an input or output endpoint is a validation error.

Each connection transports exactly one typed value. A callable used directly as a target must therefore accept one compatible flow input, and a callable used as a source must produce one flow output.

When behavior needs several related values, those values are represented by an explicit datatype and assembled by an upstream expression. A component does not implicitly zip several branches into positional arguments or infer when all branches are complete. This rule keeps invocation order, correlation, and completion deterministic.

Zero-input triggers and multi-input joins require an explicit behavior whose contract defines their activation semantics; they are not inferred from an ordinary connection.

Without a mapper, the source output type must be assignable to the target input type under the Ocean type system.

connect submit -> lifecycle.submit

No implicit object reshaping, field projection, parsing, or lossy numeric conversion is performed.

Type identity is case-sensitive, like every other Ocean identifier: a datatype, enum, imported alias, or port named Order is not the same type as one named order. A connection whose source and target differ only in case is a type mismatch, not a coincidental match.

Use the common Ocean mapper form when source and target types differ:

connect <source> -<mapper>-> <target>

Example:

connect lifecycle.accepted -toAccepted-> accepted

The mapper must resolve to a used expression with exactly one flow input and one flow output. Its input must accept the source type, and its output must be assignable to the target type. A mapper transforms one connection; it does not become an additional independently triggered flow.

A source may feed multiple targets:

connect lifecycle.accepted -> accepted, audit.record

Writing separate connections is equivalent and is preferred when targets use different mappers:

connect lifecycle.accepted -toAccepted-> accepted
connect lifecycle.accepted -toAudit-> audit.record

Fan-out does not guarantee target execution order and is not transactional. Every branch is validated independently.

Multiple sources connected to one internal target — an expression alias, an FSM event, or a nested component’s input — activate that target independently. They are not implicitly joined, buffered, or aggregated.

Combining several results requires an explicit expression, FSM, nested component, or service-level aggregate whose contract defines completion and ordering behavior.

A public output port is different: it is the component’s observable, deterministic contract (§5.2), not an internal activation. At most one connection may target a given output port. Two connections from different sources both producing the same output leave a caller with no way to know which one filled it for a given activation, and are rejected as ambiguous competing producers, whether or not the sources are actually mutually exclusive at runtime. Route both through an explicit upstream expression, FSM, or nested component first, and connect its single result to the output.

The same <source> -> <target> edge declared more than once is redundant if every declaration agrees — same mapper, or no mapper on any of them — and ambiguous if they disagree. Fan-out (§7.5) and fan-in to an internal target (above) are not affected: those are different edges, not repeats of the same one.

# redundant: the exact same edge twice
connect validate -> accepted
connect validate -> accepted
# ambiguous: which mapper applies to validate -> accepted?
connect validate -toA-> accepted
connect validate -toB-> accepted

Recursive component use is invalid, whether direct or transitive. Plain connection cycles are also invalid because they would create unbounded execution. Repetition and state transitions belong in an FSM, where their lifecycle is explicit.


A component is not an independently running process. It is instantiated by a hosting service or parent component.

For each use component declaration:

  • the host receives a logically distinct component instance;
  • component-local execution data is scoped to one activation;
  • no mutable state is retained by the component itself;
  • nested FSM state follows the FSM’s declared lifecycle and persistence model;
  • nested component instances follow the same rules recursively.

Two services using the same component definition do not share runtime state. Generated implementations may optimize stateless objects, but such an optimization must not change these semantics.

Concurrency, retries, transactions, and timeouts are not inferred at the component boundary. They are defined by the hosting service, transport, explicit FSM behavior, or other DSL contracts that own those concerns.


A component may directly use:

  • expressions;
  • FSMs;
  • other components.

A component does not directly declare:

  • implemented or used APIs;
  • brokers or topics;
  • databases, queries, or commands;
  • configuration or vaults;
  • integrations;
  • schedules;
  • deployment or ports.

The hosting service owns these resources and connects them to component ports:

@service
OrderService
use config CommonServiceConfig as cfg
impl api OrderApi as api on cfg.apiConfig.port
use component OrderAcceptance as orders
use broker OrderBroker as broker
connect api.submitOrder -> orders.submit
connect orders.accepted -> api.submitOrder
connect orders.accepted -> broker.orders.accepted

This boundary keeps a component reusable across transports, deployment environments, and infrastructure providers.

An FSM nested by a component may use persistence according to the FSM contract. That transitive dependency must remain visible in validation, generation, documentation, and architecture views; nesting must never hide operational requirements from the hosting service.


@component
AuditTrail
input : record:AuditRecord
output : recorded:AuditRecorded
use expression RecordAudit as store
connect record -> store
connect store -> recorded
OrderAcceptance
input : submit:SubmitOrderRequest
output : accepted:OrderAccepted & audited:AuditRecorded
use expression ValidateOrder as validate
use expression ToAuditRecord as toAudit
use fsm OrderLifecycle as lifecycle
use component AuditTrail as audit
connect submit -> validate
connect validate -> lifecycle.submit
connect lifecycle.accepted -> accepted
connect lifecycle.accepted -toAudit-> audit.record
connect audit.recorded -> audited

Only submit, accepted, and audited are visible to a host using OrderAcceptance. The validate, lifecycle, and audit aliases are internal.


A component file may import:

  • datatype;
  • expression;
  • fsm;
  • component.

context is intentionally not a supported component dependency: a component owns no state of its own (§8), so it has nothing to bind a context import to. The parser’s import allow-list for @component no longer accepts @import context; state remains owned by explicitly used FSMs and their contracts.

@component
@import datatype O.domain.order.SubmitOrderRequest@1.0.0 as SubmitOrderRequest
@import expression O.domain.order.ValidateOrder@1.2.0 as ValidateOrder
@import fsm O.domain.order.OrderLifecycle@2.0.0 as OrderLifecycle
@import component O.common.audit.AuditTrail@1.0.0 as AuditTrail

Every repository import requires as <alias>. The import alias is the definition name available to declarations in the file. Local use aliases still define instance names:

OrderAcceptance
input : submit:SubmitOrderRequest
use expression ValidateOrder as validate
use fsm OrderLifecycle as lifecycle
use component AuditTrail as audit

Repository-level @include is processed according to the common include contract. It is distinct from nested component use: inclusion makes definitions available to the file, while use component instantiates a component inside another component.


Component validation must report as many independent issues as possible in one validation pass.

The list below is the required target validation contract. The current registry implements all of it: unique component names; resolution of declared expressions, FSMs, and nested components; the presence of a connection source and target; import kind/alias/resolution errors; port rules (unique names, resolvable datatypes, at least one port, _ rejected); alias rules (required, unique, no conflict with a port); connection reference resolution, including encapsulation — a nested component’s internal aliases are not reachable through its alias, only its public ports; endpoint direction; source/target and mapper type compatibility (exact, case-sensitive type-identity match — see §7.3 — not yet List/Optional-aware structural compatibility); direct and transitive component-use cycles; plain connection cycles (opaque to what happens inside a used FSM or nested component — see §8); duplicate and ambiguous connections, including competing producers for one output port (§7.6, §7.6a); unused local aliases; and outputs no connection ever produces.

Validation includes:

  • valid and unique component names;
  • valid, unique public port names;
  • at least one public input or output;
  • resolvable port datatypes;
  • resolvable expressions, FSMs, and nested components;
  • required and unique local aliases;
  • valid source and target references;
  • correct endpoint direction;
  • source/target type compatibility;
  • mapper existence and mapper signature compatibility;
  • duplicate or ambiguous connections;
  • unreachable internal elements;
  • public outputs that can never be produced;
  • direct or transitive component-use cycles;
  • invalid plain connection cycles;
  • import kind, alias, and resolution errors;
  • visibility violations that reference another component’s internal elements.

Infrastructure declarations inside a component are parse or validation errors, depending on the phase in which they can be identified.


A component is generated as reusable in-process code and linked into every hosting service that uses it. It does not receive its own executable, container, health endpoint, configuration file, or deployment entry.

Generation:

  • preserves the typed public port contract: one generated method per input port, taking that port’s value and returning every declared output port (§5.2 — a Go-style multi-return where the branches this activation did not reach come back zero-valued, the same convention an FSM event already uses for its own declared outputs);
  • includes transitive expression, FSM, and nested-component dependencies (GetUsedExpressions, GetUsedFSMs, GetUsedComponents — one shared, cycle-safe traversal, covered for self-cycles, multi-component cycles, and diamond-shaped sharing);
  • creates one process-wide instance per component definition, reached by name, exactly like an FSM instance (§8’s per-host isolation therefore holds automatically between services, which are separate processes; two use component aliases for the same definition within one service share that one instance — the same restriction FSMs already have);
  • preserves activation correlation through the internal flow by compiling each connection into a direct call, threading one hop’s result into the next;
  • rejects unsupported constructs before emitting partial code: an unresolvable connection reference, or a connection graph a component-use or connection cycle check did not already reject but the FSM-bridging rule above would make cyclic, fails generation outright.

A hosting service reaches a component’s input port exactly like it reaches an FSM event or an expression — connect api.op -> alias.port compiles to a direct call, matching every other dependency kind. Component port methods do not take a context parameter, again matching FSM events; a future revision may thread one through, mirroring how an implemented API method already does.

Architecture documentation should show:

  • the component’s public inputs and outputs;
  • its internal expression, FSM, and nested-component dependencies;
  • typed and mapped connections;
  • every hosting service;
  • transitive state or persistence requirements.

In a high-level microservices view, a component belongs inside its hosting service rather than appearing as an independently deployed node. A detailed flow view may expand it into its internal graph.


1.0.0
# @ocean-meta-start
# name: order-acceptance-component
# tags:
# - order
# - reusable-behavior
# perspective:
# feature: order-management
# boundedContext: ordering
# service: shared
# @ocean-meta-end
@component
@import datatype O.domain.order.SubmitOrderRequest@1.0.0 as SubmitOrderRequest
@import datatype O.domain.order.OrderAccepted@1.0.0 as OrderAccepted
@import datatype O.domain.order.OrderRejected@1.0.0 as OrderRejected
OrderAcceptance
@perspectives: version:1.0.0, lifestyle:stable
input : submit:SubmitOrderRequest
output : accepted:OrderAccepted & rejected:OrderRejected
use expression ValidateOrder as validate
use expression ToOrderAccepted as toAccepted
use expression ToOrderRejected as toRejected
use fsm OrderLifecycle as lifecycle
connect submit -> validate
connect validate -> lifecycle.submit
connect lifecycle.accepted -toAccepted-> accepted
connect lifecycle.rejected -toRejected-> rejected

This component can be reused by an HTTP service, a broker-driven worker, or an integration host without changing its business flow.


  • A component file starts with @component.
  • A component is reusable in-process behavior, not a runtime boundary.
  • Components expose only typed input and output ports.
  • Components directly use expressions, FSMs, and nested components.
  • Every dependency has a unique explicit alias.
  • Connections use the common connect and -mapper-> grammar.
  • Every connection is directional and type-checked.
  • Fan-out is allowed; implicit aggregation and ordering are not.
  • A public output port has at most one producing connection; type identity is case-sensitive, and a redundant or conflicting edge is rejected.
  • Components do not own mutable state; FSMs own state.
  • Component instances and their stateful dependencies are isolated per host.
  • Direct or transitive recursive component use is invalid.
  • APIs, brokers, databases, configuration, vaults, schedules, integrations, ports, and deployments remain service-level concerns.
  • A component is generated into its host and is never deployed independently.
  • Internal aliases and connections are encapsulated from hosts.

  • dsl.datatype — defines component port and dependency types.
  • dsl.expression — defines stateless logic and connection mappers.
  • dsl.fsm — defines stateful behavior composed by components.
  • dsl.service — hosts components and connects them to runtime resources.
  • dsl.import — imports repository definitions into a component file.
  • dsl.include — makes reusable definitions available during composition.