<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matthew Burrows</title>
    <description>The latest articles on DEV Community by Matthew Burrows (@mattyb2001uk).</description>
    <link>https://dev.to/mattyb2001uk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3934419%2F52b9fd09-78d9-4c14-9573-e5216413d27c.jpg</url>
      <title>DEV Community: Matthew Burrows</title>
      <link>https://dev.to/mattyb2001uk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mattyb2001uk"/>
    <language>en</language>
    <item>
      <title>I Built an Enterprise Workflow Engine Like a Game Engine — And It Was the Right Call</title>
      <dc:creator>Matthew Burrows</dc:creator>
      <pubDate>Sat, 16 May 2026 08:14:40 +0000</pubDate>
      <link>https://dev.to/mattyb2001uk/i-built-an-enterprise-workflow-engine-like-a-game-engine-and-it-was-the-right-call-3p6e</link>
      <guid>https://dev.to/mattyb2001uk/i-built-an-enterprise-workflow-engine-like-a-game-engine-and-it-was-the-right-call-3p6e</guid>
      <description>&lt;p&gt;Some of the best engineering decisions I’ve made weren’t inspired by architecture books, design pattern catalogues, or conference talks. They came from somewhere else entirely.&lt;/p&gt;

&lt;p&gt;I’ve been fascinated by game development since I was a boy — over thirty years ago. Not just playing games, but understanding how they work underneath. In my spare time, over the years, I built two game engines from scratch: a 2D tile-based engine and a 3D voxel engine. No tutorials, no frameworks. Just research, curiosity, and a lot of iteration.&lt;/p&gt;

&lt;p&gt;That might sound unrelated to enterprise patch governance software. It isn’t.&lt;/p&gt;

&lt;p&gt;When I sat down to design the workflow engine at the heart of OPUS — an orchestration platform built on .NET 10, Blazor Server, and Azure Update Manager — I needed something flexible enough to reshape itself at runtime, extensible enough that adding new capabilities wouldn’t require touching the core, and clean enough that any feature could exist inside the workflow or as a standalone tool without knowing the difference.&lt;/p&gt;

&lt;p&gt;I’d solved that problem before. Not in enterprise software. In a game engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECS Isn’t a Game Pattern. It’s a Composition Pattern.
&lt;/h2&gt;

&lt;p&gt;Entity Component System — ECS — is the architectural pattern that powers most modern game engines. If you’ve used Unity, you’ve used it. The idea is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entities are pure identity containers. They hold no behaviour and no logic. They are, essentially, a name and a bag of metadata.&lt;/li&gt;
&lt;li&gt;Components are where all state and behaviour live. You attach them to entities to give those entities capabilities.&lt;/li&gt;
&lt;li&gt;Systems act on entities by querying for the components they care about, at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is an architecture built entirely on composition rather than inheritance. There are no deep class hierarchies. There’s no brittle base class trying to anticipate every future requirement. You add a new capability by writing a new component and attaching it — the rest of the engine doesn’t need to know.&lt;/p&gt;

&lt;p&gt;Games discovered ECS because they had brutal requirements: hundreds of entity types, runtime flexibility, the ability to combine behaviours in ways the original designer never anticipated. Those aren’t uniquely game requirements. They’re just requirements that games hit first.&lt;/p&gt;

&lt;p&gt;Enterprise software hits them too. It usually just reaches for the wrong tools when it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem I Was Solving
&lt;/h2&gt;

&lt;p&gt;OPUS needed a wizard-style workflow — a guided, multi-step process that walks a patch governance team through an entire patching cycle. Assessment scans, runbook scheduling, device discovery, OS patching, SQL remediation. Each of those is a distinct phase, and each phase is a sequence of steps.&lt;/p&gt;

&lt;p&gt;Simple enough on paper. But the requirements made a static approach unworkable.&lt;/p&gt;

&lt;p&gt;The workflow needed to change shape based on how the platform was configured. A tenant with only lower-tier environments gets a different workflow to one with both tiers. Automatic assessments enabled? Remove those phases entirely. SQL offset patching switched on? Inject those stages. Runbooks configured for an environment? Add those steps. No runbooks? Skip them. The number of phases, the steps inside each phase, the dates attached to them — all of it needed to emerge from configuration, not be hardcoded.&lt;/p&gt;

&lt;p&gt;On top of that, individual capabilities needed to exist outside the workflow entirely. A device exemption manager, an operation history viewer, a KB install tool — these aren’t workflow steps, they’re tools. But they’re built from the same components as the workflow phases. Writing them twice, or creating a parallel architecture just for tools, wasn’t acceptable.&lt;/p&gt;

&lt;p&gt;What I needed was a core engine that had no opinion about what the workflow looked like. Something where composition was the mechanism, not inheritance. Where adding a new capability meant writing the capability — not modifying the engine to accommodate it.&lt;/p&gt;

&lt;p&gt;That’s when I stopped thinking about enterprise architecture patterns and started thinking about game engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mapping
&lt;/h2&gt;

&lt;p&gt;Here’s how ECS maps directly onto the OPUS engine. These aren’t analogies — they’re the actual types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase is the Entity.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Phase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Steps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Phase knows its name, holds a list of navigation steps, and carries a metadata dictionary. That’s it. No behaviour, no business logic, no Azure calls. It has no idea what it represents operationally. It’s a container — exactly what an entity should be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BaseFeature is the Component.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every capability in OPUS — runbook scheduling, device discovery, patch assessment, KB management — is a class that inherits from BaseFeature. All the state lives there. All the logic lives there. The feature knows everything about its own domain and nothing about the workflow it might be hosted in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseFeature&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotifyPropertyChanged&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// State, logic, and decorated properties on every subclass&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WorkflowShellService is the System.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where it gets interesting. When the user navigates to a step, the shell service resolves which feature that step belongs to — at runtime, by type — and activates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;featureType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMetadata&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"HostTypeObject"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_currentPhaseEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;featureType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;BaseFeature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Two lines. The system queries the entity for the component it needs, by type, at runtime. If you’ve worked with ECS before, that pattern is immediately familiar. The shell service has no hardcoded knowledge of any specific feature. It just knows how to ask an entity what it’s carrying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metadata dictionary is the tag store.
&lt;/h2&gt;

&lt;p&gt;Every step declares which feature owns it and what UI control to render, purely through metadata entries. The engine reads those at navigation time. No switch statements, no feature-specific branching in the core — just a generic lookup that works identically for every feature that has ever been added or ever will be.&lt;/p&gt;

&lt;p&gt;The result is that adding a new capability to OPUS means writing a new BaseFeature subclass and registering it. The engine doesn’t change. It never needs to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Proof — The Workflow Builds Itself
&lt;/h2&gt;

&lt;p&gt;Elegant architecture is one thing. Architecture that earns its complexity under real-world requirements is another. This is where ECS stopped being an interesting design choice and became the only reasonable one.&lt;/p&gt;

&lt;p&gt;OPUS tenants configure their patch governance — which Azure subscriptions they manage, which environments exist, whether automatic assessments are enabled, whether SQL offset patching is required, whether runbooks are configured. That configuration is live. It can change. And when it does, the workflow needs to reflect it immediately.&lt;/p&gt;

&lt;p&gt;There are no JSON workflow definition files in OPUS. There is no hardcoded phase list. Instead, there’s a WorkflowDefinitionBuilder that reads governance configuration and derives the entire workflow from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;WorkflowDefinitionDto&lt;/span&gt; &lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lowerEnvs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetConfiguredEnvironments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upperTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upperEnvs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetConfiguredEnvironments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upperTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildPhasePlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;lowerEnvs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;upperEnvs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;TierHasRunbooks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lowerEnvs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;TierHasRunbooks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upperEnvs&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;phases&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PhaseDefinitionDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;BuildPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;WorkflowDefinitionDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Phases&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phases&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The builder interrogates governance and constructs a phase plan — a description of which phases should exist and what they should contain. That plan is then materialised into a full workflow definition: phases, steps, metadata, due dates, reminder targets, all wired together.&lt;/p&gt;

&lt;p&gt;When a tenant enables SQL offset patching, those phases appear. When automatic assessments are switched on, assessment phases are removed — they’re no longer needed. When runbooks aren’t configured for an environment, those steps are simply not injected. The workflow that emerges is always correct for that tenant’s configuration, and the engine that runs it never changed at all.&lt;/p&gt;

&lt;p&gt;This is the part that a static approach cannot survive. A hardcoded workflow either gives every tenant every phase regardless of relevance, or it requires conditional rendering logic scattered throughout the navigation layer. Neither is acceptable in a governance platform where what you show carries compliance weight.&lt;/p&gt;

&lt;p&gt;Because the engine operates purely on composition — phases as containers, features as components, the system resolving everything at runtime — the workflow definition is just data. The builder produces it, the engine consumes it, and the two have no direct knowledge of each other.&lt;/p&gt;

&lt;p&gt;The engine doesn’t know what a runbook is. It doesn’t know what SQL offset patching means. It just knows how to navigate a phase that contains whatever components were attached to it.&lt;/p&gt;

&lt;p&gt;That separation is what makes the whole thing work. And it came directly from thinking about how a game engine handles a level it’s never seen before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13h8abzahrftzujkvy6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13h8abzahrftzujkvy6h.png" alt="Dynamically Populated Workflow" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Features Don’t Need the Workflow
&lt;/h2&gt;

&lt;p&gt;One of the quieter benefits of this architecture only becomes obvious once you’ve lived with it for a while.&lt;/p&gt;

&lt;p&gt;Because a BaseFeature subclass is just a component — self-contained, with no dependency on the workflow that might host it — it can be instantiated and driven from anywhere. The workflow doesn’t own features. It just happens to be one place they can run.&lt;/p&gt;

&lt;p&gt;In OPUS, this means the same feature that appears as a step inside a patch cycle phase can also be surfaced as an appbar tool, an ad-hoc utility page, or a background operation. The device exemption manager, the operation history viewer, the KB install and uninstall tools — none of those are workflow steps. But they’re built from exactly the same components as everything inside the workflow.&lt;/p&gt;

&lt;p&gt;There’s no parallel architecture for tools. There’s no “workflow version” and “standalone version” of the same capability. You write the feature once. Where it runs is just a matter of where you choose to surface it.&lt;/p&gt;

&lt;p&gt;This is the same reason a component in Unity works outside of a scene. The component doesn’t know or care whether it’s inside a scene graph or being driven directly. It just does its job when something tells it to.&lt;/p&gt;

&lt;p&gt;In practice this means OPUS can grow in any direction without the core engine becoming a bottleneck. New workflow phase? New tool? New appbar shortcut? It’s all the same mechanism. The engine has no opinion about any of it.&lt;/p&gt;

&lt;p&gt;That flexibility wasn’t planned as a feature. It was a consequence of getting the composition right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgxurg3lm2toj2d37ndgm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgxurg3lm2toj2d37ndgm.png" alt="Phase Outside the Workflow" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;ECS didn’t come from an enterprise architecture book. It didn’t come from a .NET design patterns course or a conference talk about clean code. It came from thirty years of being genuinely curious about how games work under the hood — and eventually building a couple of engines to find out.&lt;/p&gt;

&lt;p&gt;When I sat down to design OPUS, I wasn’t thinking “what enterprise pattern fits here?” I was thinking “I’ve solved this problem before.” A system that needs to compose behaviour flexibly, reshape itself at runtime, and let components exist independently of the structure that hosts them — that’s not a new problem. It’s just usually described in a different vocabulary.&lt;/p&gt;

&lt;p&gt;The lesson I’d take from this isn’t “use ECS in your enterprise software.” It’s narrower and more useful than that.&lt;/p&gt;

&lt;p&gt;The patterns worth reaching for aren’t always the ones in the book you’re supposed to have read. Sometimes the right mental model is sitting in a completely different domain, waiting for you to notice that the problems are the same shape. You only notice that if you’ve spent time in both places.&lt;/p&gt;

&lt;p&gt;Years of experimenting writing game engines in my spare time from scratch didn’t make me a game developer. But it quietly shaped how I think about composition, flexibility, and what an engine should and shouldn’t know about the things it runs.&lt;/p&gt;

&lt;p&gt;That turned out to matter quite a lot.&lt;/p&gt;

&lt;p&gt;If you’re building something and the standard patterns feel like they’re fighting the problem rather than solving it — look sideways. The right idea might be somewhere you wouldn’t expect to find it.&lt;/p&gt;

&lt;p&gt;And keep your side interests. You genuinely never know when they become load-bearing.&lt;/p&gt;

&lt;p&gt;Matthew Burrows • Cloudframe Solutions Limited • &lt;a href="https://dev.tourl"&gt;opus-orchestrator.co.uk&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
