Relationships are how the fleet hangs together — and they're load-bearing in a way that surprises people. They feed derived metrics, they enforce cardinality, and they're what the cascade walks to tear infrastructure down in the right order. A sloppy relationship model is a fleet that leaks on churn.
This page is about designing relationships and ordering so the cascade does the right thing without surprises. The field references are RelationshipType, Relationship, and the Action cascade/dependsOn sections.
Do model "tenant runs on cluster" as a runs_on relationship type with from: Tenant, to: TenantCluster.
Don't stuff a cluster_id string property onto Tenant and call it done.
Because a property is dumb storage; a relationship is enforced structure. Only a relationship gives you cardinality enforcement, a feed into derived metrics (tenant-count counts active runs_on edges), and cascade ordering. A cluster_id string can point at a cluster that doesn't exist, lets one tenant claim two clusters, and teaches the cascade nothing about dependency order.
Do set cardinality to the real multiplicity and let Terrantula enforce it:
Don't default everything to many-to-many because it "can't reject anything."
Because cardinality is enforced, not advisory. many-to-one makes Terrantula reject a second active cluster for one tenant — a real consistency guarantee. many-to-many (the schema default) accepts everything, so it's the right choice only when the link genuinely is many-to-many. Picking the loose default to dodge a rejection just defers a data-integrity bug to production.
| Cardinality | Reads as |
|---|---|
one-to-one | exactly one on each side |
one-to-many | one from, many to |
many-to-one | many from, one to (the classic tenant→cluster) |
many-to-many | many on both sides; only when it's truly unconstrained |
Do carry per-link facts as relationship properties:
Don't push the per-tenant namespace onto the Tenant entity or the TenantCluster entity.
Because the namespace is a fact about this tenant on this cluster — not about the tenant alone (it'd be wrong after a migration) and not about the cluster alone (it has many). The relationship is the only place it's true. This also keeps migrate-relationship clean: re-point the edge and the link facts travel with it.
Do give the relationship its own state machine, ordered the way edges actually travel:
Don't reuse the entity's states verbatim or list them in arbitrary order.
Because the relationship has its own lifecycle, independent of the entities it joins, and the first state in states is the default for a new edge — order matters. Your cascade rules transition edges between these states (active → removing → removed), so the state names and order have to support the teardown sequence you're about to design.
This is the most common point of confusion, so be explicit. There are two ways relationships drive ordering, and they solve different problems.
cascadeRules — transition edges as an entity changes stateUse cascadeRules on an Action's transition-entity or create-entity operation when changing an entity's state should side-effect onto its relationships:
Because each rule fires at a well-defined phase — on-trigger, on-success, on-failure — keeping the edge's lifecycle in lockstep with the entity's. Transition on on-trigger for things that should reflect intent immediately (a freed capacity slot); transition on on-success for things that should only reflect committed reality (the link is truly gone).
dependsOn — sequence Actions across a relationshipUse dependsOn on an Action when applying one entity requires an upstream entity to be applied first:
Because this reads "before this Action runs, the entity at the other end of runs_on must have had ApplyTenantCluster succeed." It's how a multi-stack onboard sequences itself — cluster before the tenant that runs on it — without you ordering runs by hand. Both fields are literal name references validated at apply; the dependency graph (including edges from prior applies) must be acyclic, or you'll get DEPENDS_ON_CYCLE.
cascadeRules move relationships when an entity changes state. dependsOn sequences Actionsacross the relationship graph. One is intra-run side effects; the other is cross-run ordering.
Do trace, on paper, the order infrastructure must be destroyed when a tenant churns, then encode it with cascade phases and dependsOn.
Don't assume "delete the tenant" cleans up everything downstream for free.
Because deprovisioning rot — resources quietly leaking on the bill after a customer leaves — is one of the core cattle pains. Because relationships carry direction and cardinality, Terrantula knows the dependency order and can tear down safely, nothing removed before its dependents. But you have to declare the rules; the cascade follows your model, it doesn't guess. Walk the deprovision-a-tenant how-to for the end-to-end shape.
The cascade transitions entities and relationships in the graph, and any change to real infrastructure it implies still reaches your systems the same way every other change does — through an Action that opens a PR (or fires a runner). The cascade orders the teardown; your CI applies it. Terrantula never destroys infrastructure directly.
migrate-relationship exceptionDo use migrate-relationship to rebalance — re-point a tenant's edge from a full cluster to an emptier one.
Be aware it intentionally bypasses the property-sum constraint on the relationship type.
Because a rebalance shouldn't be blocked by the very ceiling it's relieving — but that means a migrate is the one operation that can move an edge past a property-sum cap. Use it for rebalancing, not as a backdoor around capacity rules.
removed on on-trigger marks it gone before the destroy applies — if the apply fails, your graph lies. Use on-success for "committed reality," on-trigger for "intent."dependsOn cycle. Two Actions each waiting on the other deadlocks the golden path. Apply-time validation catches it (DEPENDS_ON_CYCLE); design acyclic from the start.many-to-many everywhere. The loose default that quietly permits the inconsistency you'll spend an afternoon debugging.namespace on Tenant breaks the moment you migrate; keep link facts on the link.Next: Actions & PR hygiene → — make the PRs your Actions open reviewable and safe to apply.