Software architecture is often treated as something that can be designed upfront. A structure to be drawn, documented, and then implemented.
Most experienced practitioners know this picture doesn’t hold. Architecture does not precede a system fully formed. It reveals itself gradually, as assumptions meet constraints and abstractions are forced to justify their existence.
What’s less often discussed is that architecture can also exist before it is fully understood. A system may carry its architectural core from day one, while surrounding decisions still gravitate around more convenient, but less truthful, centers.
A refactoring over the Christmas break made it clear that LaravelUi5 was such a system.
From the very beginning, its core idea was descriptive rather than imperative. The registry existed from day one as the central model that connected UI5 applications to a Laravel host. It established a shared language for artifacts, intents, and capabilities, and treated frontend modules as first-class citizens of the backend.
In Domain-Driven Design terms, the registry was already the domain model. What was still missing was a clear understanding of its invariants. What must remain true regardless of environment, tooling, or packaging.
Early on, much of the surrounding tooling still assumed that source files would always be locally available, introspectable, and structurally uniform. Something that felt natural at the time. And as long as everything lived inside the workspace, that assumption held.
The moment metadata extraction moved into host packages, POPO-based descriptors, and Composer dependencies, it stopped holding. Source availability turned out not to be a property of the domain, but of a particular development context.
That tension did not lead to a new architectural invention. Instead, it forced a clarification of what had been there all along; that the registry represents the ubiquitous language of the system; that descriptors express stable domain knowledge rather than transient file layouts; and that a model can only serve as a source of truth if it remains valid even when its original sources are no longer present.
What followed was not a redesign, but a realignment. Not the addition of structure, but the removal of convenient illusions.
In hindsight, this followed a familiar pattern: once the domain model becomes explicit, everything else must either align with it or disappear.
This is the story of how LaravelUi5 learned to treat its own domain model seriously and what happened once it did.
Table of Contents
- The Innocent Beginning
- When Source Availability Stops Being Obvious
- The False Center of Truth
- Strategy Over Conditionals
- A Familiar Shape
- The Shape of the Domain
- The Calm After Clarification
¶The Innocent Beginning
The original trigger for everything that followed had little to do with architecture.
It started with a practical question.
How can we reliably introspect a UI5 application — not just structurally, but semantically?
Specifically, the goal was to read manifest.json files and understand what an application actually offers:
its routes, its targets, the concrete XML views it exposes.
Not as a static snapshot, but as a live representation that reflects the current development state of an app.
This mattered for a simple reason. UI5 applications evolve constantly during development, and any tooling that pretends otherwise quickly becomes misleading. If LaravelUi5 was going to treat UI5 apps as first-class citizens, it needed to honor that reality.
At first, this didn’t look like a difficult problem.
Workspace introspection already worked. It discovered metadata, created POPOs, and registered them centrally. The registry approach did exactly what it was supposed to do: it turned a collection of files into a self-describing system.
There was no need to distinguish between development stages. No special handling. No conditional logic.
As long as everything lived in the workspace, the system behaved as intended.
This is where the first assumption quietly settled in: that live introspection and source availability were naturally aligned.
They were… but only accidentally.
The desire for better ui5:app and ui5:lib commands emerged directly from this success.
If introspection already worked, why not use it more aggressively?
Why not let commands rely on it instead of reimplementing logic?
Why not treat the registry as the single interface to what an app is, rather than how it is scaffolded?
At this stage, nothing felt experimental. Nothing felt risky. The system was already doing the right thing.
What we didn’t realize yet was that we were relying on a property that did not belong to the domain at all. It belongs only to the workspace.
That realization came later, when live introspection was asked to work in a world where sources still exist, but are no longer guaranteed to be present as sources.
¶When Source Availability Stops Being Obvious
This tension appeared the moment live introspection was asked to leave the comfort of the workspace.
As long as UI5 applications lived side by side with the Laravel host, everything aligned naturally: sources were present, file paths were meaningful, and introspection could walk the filesystem freely.
In that context, source availability felt like a given. Simply the state of the world.
That illusion held remarkably well.
It held because workspace introspection doesn’t need to ask difficult questions. Files are editable. Paths are stable. The development environment and the runtime model overlap almost perfectly.
But that overlap is accidental.
The moment UI5 apps started to be consumed as packages, the picture changed subtly at first, then fundamentally.
From the registry’s perspective, nothing was missing. Modules were still installed. Descriptors were still declared. Metadata still existed.
What changed was not existence, but access.
Source files were no longer guaranteed to be present in their original form, writable, or even structured the same way.
They were still physically there, inside the vendor folder. But they no longer belonged to the same semantic category as workspace sources.
This is where a crucial distinction surfaced.
A package can be present without being introspectable in the same way.
At first, this was easy to misinterpret. After all, Composer installs code locally. Why shouldn’t it be treated like workspace code?
The answer is simple, but easy to overlook: because packages are artifacts, not development contexts.
They are meant to be consumed, not observed live. They carry intent, not process. Stability, not mutability.
Live introspection quietly relies on mutability. On the assumption that the thing being inspected is still in flux.
That assumption held in the workspace. It never truly applied to packages.
At this point, the problem was still not framed as an architectural one. It looked like a missing case. A special path. Something that could be fixed with an extra condition.
But every attempted fix had the same smell: it tried to restore a property that never belonged to the domain in the first place.
Source availability had been mistaken for truth. In reality, it was only a characteristic of one particular development context.
The registry, on the other hand, had never relied on that assumption. It didn’t care where metadata came from. It only knew that it was coherent and complete.
The tension was no longer about introspection. It was about deciding which parts of the system describe the domain and which merely describe the workspace.
That distinction would turn out to be decisive.
A short aside: this problem is not unique
Before diving deeper, it’s worth pausing for a moment.
The tension that started to surface in LaravelUi5 is not specific to UI5, PHP, or Composer. It appears in almost every ecosystem that tries to reconcile development-time flexibility with package-based distribution.
- In Node.js, it’s the difference between a local workspace and
node_modules. - In Go, between
replacedirectives and the module cache. - In Rust, between path dependencies and crates pulled from
crates.io. - In PHP, between path repositories and
vendor/.
In all of these worlds, the same mistake is easy to make: to treat installed artifacts as if they were development sources.
Good systems don’t try to erase that difference. They acknowledge it and encode it explicitly.
LaravelUi5 hadn’t done that yet.
¶The False Center of Truth
Faced with the growing tension between workspace introspection and package consumption, the next step felt almost unavoidable.
If source availability could no longer be taken for granted, then it had to be described.
Mapped.
Declared.
The result was a familiar construct: a central configuration file.
In LaravelUi5, this took the form of .ui5-sources.php.
At first glance, it looked like the right solution. A single place to explain where sources live. An explicit contract between tooling and filesystem. A way to “restore” what packages had taken away.
It was tempting.
And it worked just well enough to be convincing.
But something about it felt off.
The problem wasn’t the file itself. The problem was the role it quietly assumed.
By introducing a source map, we had created a new center of gravity. One that started to compete with the registry.
Decisions that should have belonged to the domain model were now being delegated to configuration. Paths became authoritative. Overrides became normal. And the question “what exists?” was slowly replaced by “what is configured?”.
This is a subtle shift, but a dangerous one.
Configuration is excellent at expressing variability. It is terrible at expressing truth.
Configuration can suggest.
It can override.
It can patch.But it cannot decide.
That responsibility belongs elsewhere.
The registry had always known what modules exist, what artifacts they expose, and how they relate. It had never cared whether metadata came from live introspection, cached descriptors, or compiled arrays. It only cared that the information was coherent.
By placing a source map at the center, we inverted that relationship. The registry became a consumer of configuration. The configuration became the arbiter of reality.
Once that inversion happens, complexity doesn’t show up immediately. It accumulates quietly:
- special cases multiply,
- precedence rules appear,
- debugging turns into archaeology.
At that point, the issue was no longer about packages or introspection. It was about authority.
Which part of the system is allowed to say what is?
The uncomfortable answer was clear: we had tried to externalize a decision that only the domain model was qualified to make.
The source map was never meant to be the truth. It was a hint that had accidentally been promoted to law.
Recognizing that was the real turning point.
¶Strategy Over Conditionals
Once configuration had been exposed as a false center of truth, the next temptation was obvious.
If workspace and package behaved differently, we could simply acknowledge that difference explicitly.
A conditional here.
An environment flag there.
A small branch to restore the expected behavior.
It would have worked.
Something like:
if ($isWorkspace) {
// live introspection
} else {
// fallback behavior
}
This is the point where many systems stop thinking architecturally.
The problem is not that conditionals are wrong. The problem is what they encode.
A conditional assumes that there is one “normal” behavior, and one or more deviations from it.
But nothing about workspace versus package is a deviation. They are not modes of the same thing. They are different strategies for resolving the same intent.
That distinction matters.
At this point, another realization surfaced. Quieter, but more fundamental: paths themselves were no longer trustworthy.
A filesystem path does not describe what something is. It only describes where it happened to be found. And even that changes with context. The same module resolves to different paths depending on whether it is developed locally, installed as a dependency, cached, or compiled.
A path without semantics is just a string.
What the system actually needed to resolve was intent.
- Which module is this?
- Which descriptors belong to it?
- Which sources are authoritative in this context?
Those questions were already answered.
Just not explicitly.
The registry had always operated at that level. It never cared about paths. It cared about identity, descriptors, and relationships.
Once that was acknowledged, the correct abstraction became unavoidable.
Instead of branching on environment, LaravelUi5 introduced a strategy boundary:
Ui5SourceStrategyInterface
With concrete implementations such as:
WorkspaceStrategyPackageStrategy
This was not an exercise in abstraction for its own sake. It was a declaration of intent.
The question was no longer
Are we in development or production?
but
Which strategy is responsible for resolving sources in this context?
That single shift eliminated an entire class of problems.
- Workspace stopped being a special case.
- Packages stopped being treated as broken workspaces.
- Source availability stopped being an implicit assumption.
Each strategy became responsible for one coherent interpretation of reality. And at once eliminated flags, precedence rules, and hidden fallbacks.
Most importantly, authority returned to where it belonged.
The registry no longer depended on configuration or filesystem heuristics to decide what exists. It simply asked the active strategy to provide descriptors and remained the single source of truth.
In retrospect, this was the moment where the architecture stopped coping and started speaking clearly.
By naming responsibilities correctly.
¶A Familiar Shape
Only after the strategy boundary was in place did the broader shape of the architecture become obvious. Not because something new had been invented, but because something familiar had been rediscovered.
Once you start looking for it, the same pattern appears again and again in mature systems: a clear separation between discovery, consolidation, and consumption; between flexible introspection and stable runtime truth; between how information is gathered and how it is used.
LaravelUi5 was not an exception. It was following a well-trodden path, just in its own domain.
Doctrine: Metadata Is Not Configuration
Doctrine is perhaps the closest parallel.
At first glance, its mapping options — annotations, XML, YAML — look like competing sources of truth. In reality, they are merely different discovery strategies.
No matter how metadata is expressed, it always flows into a single place:
the metadata factory.
That factory, often wrapped in a cached variant, is the authority.
Drivers discover.
The registry decides.
Doctrine never tries to resolve entity metadata directly from annotations at runtime. It introspects once, validates, compiles and then works against a stable in-memory representation.
The resemblance is hard to miss.
LaravelUi5’s workspace and package strategies play the same role: different ways of discovering descriptors, feeding into a single registry that remains authoritative regardless of origin.
Laravel: Reflection First, Containers Always
Laravel itself applies the same discipline. Quietly and consistently.
Artisan commands rely heavily on reflection: they scan service providers, inspect routes, analyze models.
Runtime code does not.
Once the application boots, Laravel works against:
- the container,
- compiled route caches,
- resolved service graphs.
Reflection is a build-time concern. The container is the truth.
Even Laravel’s famous “magic” follows this rule: discover freely, but execute predictably.
LaravelUi5’s registry and cached descriptors mirror that separation almost exactly.
Symfony: Compiler Passes and the Disappearance of Source
Symfony makes the boundary even more explicit.
Compiler passes are allowed to inspect, rewrite, and reason about service definitions in great detail. But once compilation is complete, the result is a dumped PHP container.
At runtime, the original service definitions no longer matter. Only the compiled artifact does.
This is not an optimization. It is an architectural guarantee.
The runtime never needs to ask where a service came from – only what it is.
That distinction is precisely what LaravelUi5 had to learn to enforce.
The common thread
Across all of these systems, the same principles hold:
- Introspection is flexible, but temporary.
- Runtime models are stable, but derived.
- Registries are authoritative; sources are not.
- Variation is handled through strategies, not conditionals.
Once those principles are named, they feel almost trivial. Before they are named, systems tend to fight them.
LaravelUi5 was no different.
The difference is that the registry had been there from the start. What changed was not the foundation, but the willingness to let it fully define the system’s truth.
¶The Shape of the Domain
By the time the strategy boundary was in place, the remaining pieces started to fall into alignment.
What had initially looked like a collection of technical decisions revealed itself as a coherent domain model with a clear lifecycle.
Descriptors as domain objects
At the center of that model are descriptors.
Files like manifest.json, .library, routing definitions, or i18n metadata are often treated as frontend configuration.
Something to be read, parsed, and forgotten.
In LaravelUi5, they turned out to be something else entirely.
They describe
- what a UI5 application is,
- what it exposes,
- how it can be reached,
- and how it relates to other parts of the system.
In other words, they are domain objects.
Once that perspective clicks, several decisions become self-evident. Descriptors deserve
- stable representations,
- explicit POPOs,
- names and namespaces,
- validation,
- tests,
- and a lifecycle independent of file layout.
This is why extracting metadata into POPOs was not a convenience step, but a domain clarification.
The registry does not store paths or files.
It stores meaning.
From descriptors to phases
Seeing descriptors as domain objects also clarifies when different kinds of work belong.
LaravelUi5 naturally operates in three distinct phases.
-
Workspace Phase. This is where discovery happens. Sources are mutable. Introspection is live. The goal is completeness and developer feedback.
-
Build Phase. Discovery gives way to consolidation. Descriptors are validated, normalized, and compiled. Ambiguity is resolved here – intentionally and explicitly.
-
Runtime Phase. The system no longer explores. It consumes. Runtime reads stable, compiled metadata from the registry and acts on it predictably.
These phases are not environments. They are responsibilities.
Once they are named, many tensions disappear. Live introspection no longer competes with stability. Packages no longer pretend to be workspaces. Runtime stops caring where metadata came from.
Each phase does one thing and does it well.
The registry as the invariant
Across all phases, one element remains unchanged: the registry.
It is the invariant core of the system. The place where descriptors live independently of how they were discovered. The single source of truth that outlives source files, paths, and tooling.
Strategies feed the registry. Build steps consolidate it. Runtime consumes it.
Nothing else is allowed to decide what exists.
This is not a layering decision. It is a domain commitment.
The shape that emerges
Taken together, these realizations define the architectural shape of LaravelUi5.
- Descriptors are domain knowledge.
- Introspection is a phase, not a feature.
- Runtime consumes compiled intent, not raw sources.
- Workspace and packages are strategies, not modes.
- The registry is the source of truth.
At this point, the architecture stops being implicit.
It becomes teachable.
Documentable.
Predictable.
Simply because it was finally given a name.
¶The Calm After Clarification
Once the architecture had clarified itself, a noticeable shift occurred.
It changed how problems presented themselves.
Those questions lingering in the background, quietly accumulating tension, previously had felt heavy. Now they started to resolve almost on their own.
One of those questions was internationalization.
For a long time, it had hovered unresolved: how to manage i18n labels for reports, dialogs, dashboards, and shared UI elements spread across multiple packages, owned by different modules, yet expected to feel cohesive at runtime.
In a system without a clear domain model, this is where complexity usually compounds. Labels become scattered. Overrides appear. Conventions turn into folklore.
With the clarified model, the problem simply changed shape.
Descriptors already knew what artifacts existed. Artifacts already had stable identities. Packages were no longer special cases, but just another strategy feeding the same registry.
Once that was true, i18n stopped being a cross-cutting concern. It became another form of metadata, discovered, compiled, and consumed like everything else.
There was no special solution.
No new mechanism.
Just alignment.
The same pattern repeated elsewhere.
This is what architectural clarity tends to enable.
It doesn’t eliminate complexity.
It localizes it.
It doesn’t promise flexibility everywhere.
It creates stable points where flexibility can safely attach.
With descriptors treated as domain objects, with introspection confined to its proper phase, and with the registry acting as the invariant core, the system no longer needs to defend itself against its own growth.
New capabilities don’t require new exceptions. They simply attach to what already exists.
That is the quiet opening of doors.
The kind that makes future decisions feel lighter than they used to.
In hindsight, this is perhaps the most reliable signal of a sound architecture: when new questions arise, they no longer ask where to fit in, only what they are.
And the system already knows how to answer that.
ᮿ
Read the deep dive →
About the author
Michael Gerzabek works with engineering teams on system architecture and developer experience in complex SaaS environments.
He writes about the architectural decisions that keep systems understandable long after the first release.