Structure
Your coffee machine model has definitions, usages, attributes, and an enumeration. It describes what things are and what properties they carry. But it says nothing about how those parts interact with the outside world – where water enters the brewer, where ground beans come out of the grinder, or how many heating elements exist. This chapter fills those gaps.
You will add ports to define interaction boundaries, multiplicity to say how many of something exist, and port definitions to type exactly what flows through each boundary. By the end, your coffee machine will have a fully specified structural interface.
Composite Structure Revisited
Before adding new concepts, look at what composite structure already gives you. In Chapter 2, you built this:
part def CoffeeMachine {
part grinder : Grinder;
part brewer : Brewer;
part waterTank : WaterTank;
attribute serialNumber : String;
}
This says a CoffeeMachine is composed of three parts. The keyword part inside a definition creates an ownership relationship: the grinder, brewer, and water tank exist because the coffee machine exists. If you destroy the machine, the parts go with it. This is composite structure – the whole owns the parts.
Each part usage is typed. part grinder : Grinder does not just say “there is something called grinder.” It says the grinder conforms to the Grinder definition, which means it carries every attribute and port that Grinder declares. Type a usage, and all the definition’s features come along for free.
But right now, these three parts sit side by side inside CoffeeMachine with no way to interact. The grinder has no output chute. The brewer has no water inlet. To model interaction, you need ports.
Ports: Where Things Cross a Boundary
A port is a point on a part where interaction happens. Think of a physical connector, a pipe fitting, or an electrical terminal. In SysML v2, a port declares that something can flow into or out of a component at a specific point.
Here is the simplest possible port:
part def Brewer {
attribute waterTemp : Real;
attribute brewPressure : Real;
port waterInlet;
}
This adds an untyped port called waterInlet to the Brewer. It compiles, but it does not say anything useful. What flows through it? In what direction? To make ports meaningful, you need two things: a port definition that types the port, and a direction that says which way things flow.
Port Definitions
A port definition describes the kind of interaction point. It declares what items flow through it and in which direction.
port def WaterPort {
in item water : Water;
}
This says: a WaterPort is an interaction point where Water flows in. The direction keyword in is part of the item usage inside the port definition. Now apply it:
part def Brewer {
attribute waterTemp : Real;
attribute brewPressure : Real;
port waterInlet : WaterPort;
}
port waterInlet : WaterPort says: the brewer has an interaction point called waterInlet, and it conforms to the WaterPort definition. Because WaterPort declares in item water : Water, a reviewer (or a tool) can verify that anything connected to waterInlet must supply Water, flowing inward.
The pattern mirrors what you already know: port def defines a reusable type, port creates a usage of that type. Same definition/usage split as part def and part.
Direction: in, out, inout
Port definitions use direction keywords to declare which way items flow through a port. There are three:
in– the item flows into the owning partout– the item flows out of the owning partinout– the item flows in both directions
Direction is relative to the part that owns the port. When you write in item water : Water inside a port on the Brewer, you are saying water flows into the brewer through this port. From the perspective of whatever is connected on the other side, that same water is flowing out.
Here is what all three look like in port definitions:
port def WaterPort {
in item water : Water;
}
port def GroundCoffeePort {
out item grounds : CoffeeBeans;
}
port def StatusPort {
inout attribute level : Real;
}
WaterPort receives water. GroundCoffeePort emits ground coffee. StatusPort can both send and receive a level reading – useful for a sensor that reports values but can also be calibrated.
A single port definition can carry multiple items:
port def BrewInputPort {
in item water : Water;
in item grounds : CoffeeBeans;
}
This models a port where two distinct items enter. That is less common for physical ports (you would typically use separate connectors), but useful for logical interfaces where a single interaction point carries multiple signals.
Why Direction Matters
Direction is not decoration. It enables three things:
-
Connection validation. When you wire two ports together in Chapter 6, tools can check that an
outport connects to aninport of a compatible type. Connecting twoinports is a modeling error. -
Flow analysis. Direction tells you which way items move through the system. In Chapter 7, action flows will use port directions to determine data dependencies.
-
Interface contracts. A port definition is a contract: “this component expects to receive Water here.” Any component with a matching port can plug in. Direction is part of that contract.
If you omit direction, the item has no declared flow direction. That weakens every check that depends on knowing which way things move. Always declare direction on items inside port definitions.
Building Port Definitions for the Coffee Machine
Now apply ports to your existing model. Think about each component and ask: what crosses its boundary?
Grinder: Coffee beans go in; ground coffee comes out.
port def BeanInputPort {
in item beans : CoffeeBeans;
}
port def GroundCoffeePort {
out item grounds : CoffeeBeans;
}
Both ports carry CoffeeBeans, but at different stages. The beans that enter are whole; the grounds that exit have been processed. For now, both are typed as CoffeeBeans. In a more detailed model, you might define separate item types for whole beans and ground beans. Keep it simple until the model demands more precision.
Brewer: Water and ground coffee go in; brewed coffee comes out.
port def WaterPort {
in item water : Water;
}
port def BrewOutputPort {
out item coffee : BrewedCoffee;
}
WaterTank: Water goes out (to the brewer).
port def WaterSupplyPort {
out item water : Water;
}
Notice that WaterPort and WaterSupplyPort both involve Water, but in opposite directions. WaterPort receives water (in); WaterSupplyPort provides water (out). When you connect them in Chapter 6, the directions will match: one outputs what the other inputs.
Adding Ports to Existing Parts
With port definitions in hand, update the part definitions from Chapter 2. You are not redefining these parts – you are extending them with ports:
package CoffeeMachineDomain {
import ScalarValues::*;
// Items
item def Water;
item def CoffeeBeans;
item def BrewedCoffee;
// Enumerations
enum def GrindSetting {
fine;
medium;
coarse;
}
// Port definitions
port def BeanInputPort {
in item beans : CoffeeBeans;
}
port def GroundCoffeePort {
out item grounds : CoffeeBeans;
}
port def WaterPort {
in item water : Water;
}
port def WaterSupplyPort {
out item water : Water;
}
port def BrewOutputPort {
out item coffee : BrewedCoffee;
}
// Parts with ports
part def Grinder {
attribute grindSize : GrindSetting;
port beanInput : BeanInputPort;
port groundOutput : GroundCoffeePort;
}
part def Brewer {
attribute waterTemp : Real;
attribute brewPressure : Real;
port waterInlet : WaterPort;
port groundInput : BeanInputPort;
port brewOutput : BrewOutputPort;
}
part def WaterTank {
attribute currentLevel : Real;
attribute maxCapacity : Real;
port waterOut : WaterSupplyPort;
}
part def CoffeeMachine {
part grinder : Grinder;
part brewer : Brewer;
part waterTank : WaterTank;
attribute serialNumber : String;
}
}
Look at what the model says now. The Grinder accepts beans through beanInput and emits grounds through groundOutput. The Brewer receives water and grounds, then outputs brewed coffee. The WaterTank supplies water. Every boundary is named, typed, and directional.
Notice that Brewer.groundInput reuses BeanInputPort. That is the same port definition used by Grinder.beanInput. Reuse is the point of port definitions: define the interface once, use it on any part that needs it. The Brewer accepts beans (ground) through the same type of input port as the Grinder, because structurally the interface is the same – coffee material flowing inward.
This general view shows the structural definitions with their ports and typing relationships:
sysml-rs extends the spec here. Container nodes in this book’s diagram fixtures (a
part defwith attributes inside, a port with a typed item flowing through it) can be expanded or collapsed. A collapsed node shows only its name and outline; an expanded node shows its members. The expand/collapse contract is a sysml-rs presentation convention — the spec graphical notation does not prescribe it. Reach for collapsed nodes when you want a high-level architectural read; expand them when you want the full attribute and port inventory.
Enumerations: A Brief Recap
Chapter 2 introduced enum def GrindSetting with three values. Enumerations are attribute definitions with a fixed set of members. A few additional patterns are worth knowing.
Enumeration values can carry data. The data is declared once, on an attribute definition, and each value fills it in:
attribute def BrewRatio {
attribute ratio : Real;
}
enum def BrewStrength :> BrewRatio {
enum light { :>> ratio = 15.0; }
enum medium { :>> ratio = 12.0; }
enum strong { :>> ratio = 10.0; }
}
BrewStrength specializes BrewRatio, so it inherits ratio; each value then redefines ratio with its own number. Any attribute typed by BrewStrength can be asked for its ratio, because there is one ratio feature rather than three.
Resist the temptation to declare attribute ratio : Real inside the enumeration definition itself. Setting aside annotations – a doc or a comment is always welcome – an enumeration definition’s body holds only its enumeration values (SysML §7.8.2). The definition is a set of alternatives, not a place to hang features. Declaring a fresh ratio inside each value instead would parse, but it would give you three unrelated attributes and nothing common to query.
You can reference enum members using qualified names:
BrewStrength::strong
GrindSetting::fine
This is the same :: scoping syntax you use with packages and imports. An enum definition is a namespace; its members are named elements inside that namespace.
For most modeling work, simple enumerations like GrindSetting are sufficient. Use attributed enumerations when the domain genuinely needs data attached to each variant.
Multiplicity
So far, every usage in the model means “exactly one.” The coffee machine has one grinder, one brewer, one water tank. But real systems are not always that simple. A machine might have multiple heating elements, zero or more optional accessories, or a sensor array with a specific count.
Multiplicity declares how many instances of a usage can exist. You write it in square brackets, most commonly right after the type – though the specification also allows it directly after the name, or after a :> or :>> clause, and only one multiplicity per usage (SysML §7.6.3):
part def Brewer {
attribute waterTemp : Real;
attribute brewPressure : Real;
part heatingElement : HeatingElement [2];
port waterInlet : WaterPort;
port groundInput : BeanInputPort;
port brewOutput : BrewOutputPort;
}
part heatingElement : HeatingElement [2] says: every Brewer contains exactly two heating elements. Not one, not three – two.
Multiplicity Notation
The general form is [lower..upper]. Common patterns:
| Notation | Meaning |
|---|---|
[1] | Exactly one (common explicit singleton intent) |
[2] | Exactly two |
[0..1] | Optional – zero or one |
[1..*] | One or more |
[0..*] | Any number, including zero |
[2..4] | Between two and four |
The * means “unbounded.” [0..*] is the most permissive multiplicity: any number of instances.
When You Omit Multiplicity
If you write part grinder : Grinder with no brackets, you almost always get [1..1] anyway. The specification declares an implicit [1..1] when all three of these hold (SysML §7.6.3):
- the usage is an attribute usage, an item usage (including a part usage, but not a connection usage), or a port usage;
- it is owned by a definition or another usage – not directly by a package;
- and it declares no subsetting or redefinition of its own.
Every usage in the coffee machine model meets all three, so the model already means “exactly one” everywhere. When those conditions do not hold – a usage sitting directly in a package, or one that subsets an inherited collection – the usage takes the bounds of whatever it subsets or redefines, falling back to the most general [0..*] if nothing tighter is inherited.
Writing [1] therefore changes nothing about the meaning. It makes the intent visible:
part def CoffeeMachine {
part grinder : Grinder [1];
part brewer : Brewer [1];
part waterTank : WaterTank [1];
}
This is more verbose but leaves nothing to a reader’s memory of the default rule. Use explicit multiplicity when the count matters for analysis, and leave it off when “one” is obvious from context.
Multiplicity on Attributes and Ports
Multiplicity is not limited to parts. Attributes and ports support it too:
part def SensorArray {
attribute readings : Real [0..*];
port outlets : PowerOutlet [6];
}
readings carries zero or more values. outlets provides exactly six connection points. You will not need multi-valued attributes or ports for the coffee machine, but the syntax is the same wherever multiplicity appears.
Attribute Definitions and Default Values
Chapter 2 used Real, String, and GrindSetting as attribute types. Two additional patterns are worth knowing before you see the full model.
Attribute definitions create reusable value types, the same way part def creates reusable component types:
attribute def TemperatureRange {
attribute min : Real;
attribute max : Real;
}
Then use it on a part: attribute operatingTemp : TemperatureRange. This bundles related properties into a single named attribute instead of scattering loose fields.
Values attach a number to an attribute, and there are three ways to do it that mean three different things:
part def WaterTank {
attribute currentLevel : Real := 0.0;
attribute maxCapacity : Real default = 1500.0;
attribute nominalPressure : Real = 1.0;
port waterOut : WaterSupplyPort;
}
:=is an initial value.currentLevelstarts at zero and changes as the tank fills.default =is an overridable default.maxCapacityis 1500.0 unless a particular usage says otherwise. Writingdefault 1500.0, with the=elided, means the same thing.- A bare
=is a binding. It asserts that the attribute is that value, for every instance, at all times.
The bare = is the one that catches people out. attribute currentLevel : Real = 0.0 does not say “starts empty” – it asserts the level is always zero (SysML §7.6.3). Reach for = when the value is a genuine constant of the type, := when it is a starting point, and default = when a usage should be free to override it. For computed values derived from other properties, see Chapter 9.
The Complete Model So Far
Here is the full coffee machine model after this chapter. Everything from Chapter 2 is preserved; ports, port definitions, and explicit multiplicity are the additions:
package CoffeeMachineDomain {
import ScalarValues::*;
// Items: things that flow through the system
item def Water;
item def CoffeeBeans;
item def BrewedCoffee;
// Enumerations
enum def GrindSetting {
fine;
medium;
coarse;
}
// Port definitions: typed interaction boundaries
port def BeanInputPort {
in item beans : CoffeeBeans;
}
port def GroundCoffeePort {
out item grounds : CoffeeBeans;
}
port def WaterPort {
in item water : Water;
}
port def WaterSupplyPort {
out item water : Water;
}
port def BrewOutputPort {
out item coffee : BrewedCoffee;
}
// Structural definitions with ports
part def Grinder {
attribute grindSize : GrindSetting;
port beanInput : BeanInputPort;
port groundOutput : GroundCoffeePort;
}
part def Brewer {
attribute waterTemp : Real;
attribute brewPressure : Real;
port waterInlet : WaterPort;
port groundInput : BeanInputPort;
port brewOutput : BrewOutputPort;
}
part def WaterTank {
attribute currentLevel : Real := 0.0;
attribute maxCapacity : Real default = 1500.0;
port waterOut : WaterSupplyPort;
}
part def CoffeeMachine {
part grinder : Grinder [1];
part brewer : Brewer [1];
part waterTank : WaterTank [1];
attribute serialNumber : String;
}
}
Count the ports. The grinder has two (beans in, grounds out). The brewer has three (water in, grounds in, coffee out). The water tank has one (water out). Together, these ports define every boundary where material crosses from one subsystem to another.
The CoffeeMachine itself does not have ports yet. That is intentional – the machine’s external interface (where the user puts beans in, where the cup sits) is a design decision for later. Right now, the internal boundaries are what matter for wiring subsystems together.
What Comes Next
The parts have boundaries, but nothing connects them. The grinder’s groundOutput and the brewer’s groundInput are compatible – they both involve CoffeeBeans flowing in matching directions – but the model does not say they are wired together.
Chapter 6 adds connections and interfaces to wire these ports together, completing the structural picture. The ports you defined here are the endpoints; Chapter 6 provides the wires.
Common Mistakes
Forgetting direction on port items. This compiles:
port def WaterPort {
item water : Water;
}
But without in, out, or inout, tools cannot validate connections or analyze flow direction. Always specify direction.
Defining ports without port definitions. You can write port waterInlet; with no type, but that tells tools nothing about what flows through it. An untyped port is like an untyped attribute – technically valid, practically useless.
Mismatched directions at connection time. If you define WaterPort as in and WaterSupplyPort as in, you cannot connect them – both sides are trying to receive. One side must output what the other inputs. Think of directions as pipe fittings: a supply end connects to a receiving end.
Over-specifying multiplicity. Writing [1] on every single usage adds noise without information when the count is obvious. Use explicit multiplicity when it conveys design intent: [2] for dual heating elements, [0..1] for optional accessories, [1..*] for extensible arrays. Leave it off when “one” is self-evident.