References
LikeC4 uses the lexical scope, with hoisting, almost like in JavaScript.
Scope
To understand references, we need to understand scopes first.
Example:
model {
service service1 {
component api
component frontend
}
}
Every element is unique in the model, so we can add a relationship referencing them, like:
model {
service service1 {
component api
component frontend
}
frontend -> api
}
But if we add service2
with another api
:
model {
service service1 {
component api
component frontend
}
service service2 {
component api
}
frontend -> api // ⛔️ Error: 'api' not found
}
The reference is ambiguous, as there are two api
components in the model.
Every element creates a new scope inside {...}
, so api
is unique inside service1
and service2
,
but not in the scope of model
.
We can resolve by moving the relationship to the scope of service2
:
model {
service service1 {
component api
component frontend
}
service service2 {
component api
frontend -> api // ✅ This is OK,
// 'api' is unique in 'service2'
// 'frontend' is unique in 'model'
}
}
Hoisting
Hoisting is a mechanism that moves the reference to the top of the scope.
In LikeC4, the element, besides being hoisted in its scope, also "bubbles" to the upper scopes, if it stays unique.
We may reference something that is not yet defined but will be hoisted later.
The relationship on line 8 references graphql
defined below on line 15:
model {
service service1 {
component api
component frontend
frontend -> api // ✅ This is OK, references to 'api' from 'service1'
frontend -> graphql // ✅ This is OK, references to unique 'graphql'
}
frontend -> api // ⛔️ Error: 'api' is ambiguous
service service2 {
component api
component graphql
frontend -> api // ✅ This is OK, references to 'api' from 'service2'
}
}
Lines 7 and 17 are the same: frontend -> api
But they reference to different elements:
- Line 7 to service1.api
- Line 17 to service2.api
Fully qualified names
Top-level elements (placed directly in the model
block) are available globally.
To reference nested elements we use their fully qualified names (FQN).
Example:
model {
service service1 {
component api
component frontend
}
service service2 {
component api
}
frontend -> api // ⛔️ Error: 'api' not found
frontend -> service1.api // ✅ This is OK
frontend -> service2.api // ✅ This is OK
}
Or even:
model {
service service1 {
component api
component frontend {
-> service2.api
}
}
service service2 {
component api
}
}
Some parts may be omitted, if FQN stays unique:
model {
service service {
component backend1 {
component api
}
component backend2 {
component api
component graphql
}
}
frontend -> service.backend1.api // ✅ Non-ambiguous fully qualified name
frontend -> backend1.api // ✅ This is OK, 'api' is unique in 'backend1',
// and 'backend1' is unique in the model
// We may omit 'service'
frontend -> backend2.api // ✅ This is also OK
frontend -> service.api // ⛔️ Error: 'api' is ambiguous in 'service'
frontend -> service.graphql // ✅ This is also OK, we omit 'backend2'
// as 'graphql' is unique in 'service'
}
While omitting FQN-parts makes code better looking and references shorter,
it may be error-prone when you refactor the model
Sure, only if you have same-named elements