"Oh Saint Cats. What Have I Done?" 🤯
It was 11:47 PM on a Tuesday. I was writing documentation for my PHP framework, exploring plugin architecture patterns, when it hit me like a freight train.
I stopped typing. Stared at my screen. And said out loud to my empty room:
"Oh Saint Cats. What have I done."
The Setup: What I Thought I Was Building
Just two days ago, I shared my journey of building a modular PHP framework during my career transition. Coming from network engineering, PHP and Angular development, I was frustrated by the implicit, conventional dependencies and weak module boundaries in PHP applications. Most frameworks rely on conventions for module interaction, which often leads to accidental coupling and hidden dependencies.
My goal was simple: bring explicit import/export contracts and true encapsulation to PHP web applications.
I built:
- Module-scoped DI containers (each module isolated)
- Explicit import/export contracts (dependencies visible in code)
- PowerModuleSetup pattern (cross-cutting functionality without coupling)
I thought I was solving the architectural problems of web applications.
But I had underestimated the scale of the solution.
The Realization: From Decoupled to Universal
From the beginning, I designed the framework to be decoupled from any specific domain. I proved this concept by building the HTTP router as a completely separate, optional package. I knew this design made it flexible, and I had even documented use cases for web APIs and ETL pipelines.
But the true "aha!" moment came while documenting the PowerModuleSetup
plugin system. I wasn't just looking at individual use cases anymore; I was looking at the universal pattern that connected them.
That's when the penny dropped. The realization wasn't that the core was free of web code... I knew that. It was that the combination of PowerModule
isolation and the PowerModuleSetup
extension mechanism created a repeatable, universal pattern for building entire, domain-specific ecosystems.
Connecting the Dots
My heart was racing. It wasn't a frantic brainstorm of new ideas; it was the sudden, shocking realization that my existing examples were not just disparate use cases. They were all instances of the same powerful, underlying pattern.
The Web API Use Case...
$webApp = new ModularAppBuilder(__DIR__)
->withModules(ApiCoreModule::class, UserModule::class)
->addPowerModuleSetup(new RoutingSetup()) // A web-specific setup
->build();
The ETL Pipeline Use Case...
$etlApp = new ModularAppBuilder(__DIR__)
->withModules(EtlCoreModule::class, CsvExtractorModule::class)
->addPowerModuleSetup(new PipelineSetup()) // An ETL-specific setup
->build();
A Potential CLI Tool Use Case...
$cliApp = new ModularAppBuilder(__DIR__)
->withModules(CliCoreModule::class, GitModule::class)
->addPowerModuleSetup(new CommandSetup()) // A CLI-specific setup
->build();
The same modular patterns worked for everything.
The Ecosystem Revelation
But it got bigger. Much bigger.
I realized I hadn't just built a framework. I had built a framework for building ecosystems.
-
Layer 1: Core Framework & Universal Extensions
-
power-modules/framework
: The core modular architecture. -
power-modules/plugin
(coming soon): Generic infrastructure for building plugin systems.
-
-
Layer 2: Domain-Specific Plugin Systems
-
power-cms/core
: Provides the core interfaces and plugin registry for a CMS ecosystem. -
power-gateway/core
: Provides the core for an API Gateway ecosystem. -
power-etl/core
: Provides the core for an ETL pipeline ecosystem.
-
-
Layer 3: Third-Party Plugins
-
power-cms/blog
: A blog plugin for the CMS ecosystem. -
power-gateway/auth
: An authentication plugin for the API Gateway ecosystem. -
power-etl/csv
: A CSV processing plugin for the ETL ecosystem.
-
The same PowerModuleSetup pattern could enable plugin discovery across completely different domains.
The Validation Mission
By this point, it was 1:30 AM and I was buzzing with excitement and disbelief. But I needed validation. Was this really as innovative as it felt, or was I just having a late-night coding high?
I crafted a comprehensive research prompt and submitted it for independent analysis. The task: compare my framework's architectural patterns against every major solution in the PHP ecosystem and beyond.
The research covered:
- Symfony's bundle system and compiled containers
- Laravel's service providers and packages
- WordPress's plugin architecture
- Drupal's module system
- OSGi (Java) for comparison with true modularity
- Node.js module patterns
I wanted the brutal truth.
The Verdict: Revolutionary, Not Just Useful
After extensive analysis, the independent research concluded:
"The Modular Framework is a revolutionary and unique architecture in the PHP ecosystem."
The key findings:
- Module-scoped DI containers are unique in PHP - no major framework provides true container isolation per module
- Runtime-enforced import/export contracts are unprecedented - most frameworks rely on convention, not architectural enforcement
- The PowerModuleSetup pattern enables ecosystem building - a novel approach to cross-cutting functionality
Not just useful. Not just an improvement. A genuine paradigm shift.
The Architectural Vision Document
To formalize these findings, I created an Architectural Vision Document that serves as a technical white paper, positioning the framework within the broader landscape of software architecture.
It compares the framework directly against established solutions and demonstrates why this approach represents a new paradigm for building complex, maintainable PHP systems.
What This Means for PHP Development
This discovery has massive implications:
For Large-Scale Applications
- True encapsulation prevents architectural decay
- Explicit dependencies make systems maintainable
- Team scalability through isolated module ownership
For Ecosystem Building
- Consistent plugin patterns across different domains
- Universal extension mechanisms via PowerModuleSetup
- Framework-agnostic approach to modularity
For Microservice Evolution
- Natural service boundaries defined by modules
- Clear extraction path from monolith to services
- Contract-based communication ready for HTTP APIs
The Technical Reality
Let me be clear about what this is:
- ~1,600 lines of focused, well-tested core code
- PHPStan level 8, comprehensive test coverage
- Built on proven patterns from other ecosystems
- MIT licensed, completely open source
It's not about creating the "next big framework" - it's about bringing architectural rigor to PHP that was previously unavailable.
Try the Revolution Yourself
composer require power-modules/framework
Basic Example:
class MyModule implements PowerModule, ExportsComponents
{
public static function exports(): array
{
return [MyService::class];
}
public function register(ConfigurableContainerInterface $container): void
{
$container->set(MyService::class, MyService::class);
}
}
$app = new ModularAppBuilder(__DIR__)
->withModules(MyModule::class)
->build();
$service = $app->get(MyService::class);
The Journey Continues
This discovery has completely changed my perspective on what I've built. What started as personal frustration with PHP architecture has become something that could influence how we build complex backend systems.
The next steps:
- Building example ecosystems (CMS, ETL, CLI tools)
- Creating plugin developer guides
- Exploring interoperability patterns between ecosystems
Resources
- GitHub: power-modules/framework
- Architectural Vision: Technical white paper
- Research Analysis: Original research prompt
The Lesson
Sometimes the most innovative solutions come from focusing intensely on problems you truly understand. You might think you're building one thing, only to discover you've created something much more significant.
That Tuesday night reminded me why I love programming: the moment when you realize you've built something that could change how people approach entire categories of problems.
What architectural challenges have been keeping you up at night? Have you had similar "aha!" moments in your development journey?
The discovery that a personal project has broader implications than you ever imagined is one of the most exhilarating experiences in software development. This is mine.
Top comments (11)
Distributed containers is a good pattern when the application needs to scale up.
I'm just wondering if the added boilerplate helps a modular monolith?
I think the interactions between modules are more prone to failures than having a single container.
I agree it is tempting to use everything available, but when you have a recipe it will be less likely to add other ingredients.
I'm also wondering what the cost is of the distributed containers? When you need to scale that cost is reduced by the need to bring more to the table than the application can handle.
It certainly is a very interesting idea to do it from the ground up.
Hey David,
Thank you so much for the comment and questions. These are really valuable moments for me, and I appreciate you diving in! I will try to address your points one by one, but please feel free to continue the discussion if you have more thoughts.
Regarding your first concern, you are right to question this. As developers, we value simplicity and often want to avoid extra work. My argument is that this framework's approach ultimately leads to a greater form of simplicity. The "boilerplate" of imports/exports is the core feature. It turns the implicit, invisible, and fragile dependencies that exist in a single-container monolith into explicit, visible, and validated contracts. Well, I believe you got the point from the post series :) But let me try to elaborate. For example, in TypeScript, the "boilerplate" of adding types feels like extra work at first, but it prevents a huge class of runtime errors.
In the same way, my framework's "contracts" (the imports/exports) are validated when the application boots. If a dependency is unmet (because it's not explicitly exported or imported), the application won't even start. It fails fast with a clear error, preventing the very inter-module failures you're rightly concerned about. The philosophy is to make dependencies explicit and validated, even if it requires a little more definition upfront. It pays off in the long run.
On a technical level, there's a neat implementation detail that makes this efficient. When a module exports a service, the root container doesn't duplicate the service or its configuration. Instead, it creates a lightweight reference that simply points back to the module's own container. When you request an exported service from the main application, the root container delegates the request to the module that owns it. This preserves accurate encapsulation: the module retains full control over its service's lifecycle and avoids any duplication of services.
Regarding your "recipe" metaphor. I love it, and it applies on multiple levels. My vision is that good architecture doesn't hide complexity; it manages it by making it explicit. In a typical kitchen (a single-container app), all ingredients are on one big counter, and you just have to trust the chef not to grab the salt instead of the sugar. My approach is like giving each chef their own pre-measured ingredient kit (imports) for the specific dish they're making. It's impossible for them to grab the wrong thing because it's not in their kit.
So, yes, it might seem more complex at first glance, but it actually reduces the cognitive load when working on a specific module. Each module has a clear contract of what it needs and what it provides, making it easier to reason about and work with in isolation.
Regarding the cost: again, perfect question. There is a small, one-time cost, but it only happens during the application's very first boot. In this initial step, the framework performs a topological sort to build the module dependency graph, and then caches the result (for now it's just a psr/cache filesystem implementation). On every subsequent request, the application boots almost instantly by reading the pre-computed graph directly from the cache. The graph is only ever recalculated if the list of registered modules changes.
This tiny, one-time cost is negligible compared to the immense long-term "cost" of maintaining a large monolith with hidden, scattered dependencies. The framework pays a tiny upfront price for massive gains in safety, testability, and long-term maintainability.
And yes, you've perfectly summarized the goal! I'm really enjoying working on the framework which started as a learning project, and my hope is that it will help others as well.
Thank you again for your thoughtful questions and insights—they really help refine and clarify the ideas behind the framework!
I don't think the dependencies are implicit or invisible, IDEs try to hide them but the use lines in the PHP files are very visible. And dependencies are very explicit in the method/function arguments.
I agree that the dependencies are fragile in the sense that they can be used everywhere. But that can also be a blessing when a refactoring is needed.
With the recipe analogy, I was thinking more about the patterns that are used with a single container and module interactions. It can go from the tight coupling of APIs between modules, over gateways/facades, until the loose coupling of events.
I think the multiple containers are in gateway/facades category because the relations are pushed to the edges. But the only concern is the relationship, as the classes are used as is.
In your white paper you mention a modular monolith with services in the future. I think if you assume that future you are over-engineering. For me a modular monolith is about separating functionalities and easy discoverability.
Breaking up a modular monolith in services in not as straight forward as moving modules to services. For example you have to reduce the chatter between modules if they become a service because network calls are expensive.
If it comes down to it, I rather have a looser system with the single container and picking the best connection pattern for module interactions for the specific application. Than starting with multiple containers and having the reassurance things break fast when it comes to relations. I'm sure there will be people who find it helpful for their applications.
I see that you have an excellent understanding, and you definitely have extensive experience in development. I really appreciate your investment of time, and this is exactly the kind of technical dialogue I was hoping to have! :)
I love that we are closely aligned on:
Some aspects of the framework or overall vision might be opinionated, as I'm quite new to this, and I am very open to feedback and suggestions.
Anyway, let me clarify some points where I may not have communicated clearly (I'm still learning how to do that effectively):
On "implicit" dependencies: you are right. What I meant is that they are not literally implicit, but rather not declared anywhere at the architecture level - there's no enforcement of domain boundaries between modules. This is more about the lack of explicit contracts while separating domains and/or scaling the team, in practice. The idea of this framework was to address that. This also contributes to less friction between teams, in my opinion.
Regarding the microservice evolution, I briefly mentioned it above - I appreciate that we're on the same page here - it confirms my thinking. To elaborate, I'm not assuming that's the only path forward nor that it's the primary goal of the framework. It simply provides you with more flexibility (or less preparation) if you ever need it down the road.
After all, I think there's a philosophical difference here. Some would prefer flexibility to choose coupling patterns per use case (and I recognize that's the prevailing approach in our community). I'm more in favour of a more structured approach. Both are valid, I believe. It comes down to whether you want the architecture to guide you or get out of your way.
I'm curious. In your experience, what patterns have you found most effective for preventing accidental coupling as teams and codebases grow? And what are your thoughts on:
Do you think there is a way to improve it without overcomplicating the architecture?
Looking forward to your thoughts!
I think everything we do is opinionated. It is not a bad thing when the opinions can change once a better solution presents itself.
It is by being opinionated an application, a framework or a library has consistency.
I think having explicit code is good, but I also consider looseness. I always remind myself of the galloping gertie bridge failure. Software needs to have a bit of wiggle room to keep it maintainable. Once it is too much in tune it will break.
Good communication. It sounds simple but I think a lot of people overlook that aspect, and that is why there are a lot of manager roles.
I'm mainly doing backend code, but I still like to talk about design and SEO/GEO with experts in those fields. At the end of the day our work comes together to create the product.
I think that focus is inherent for the problem your framework solves. If I were you I wouldn't extend that focus too much.
An option could be to export/import interfaces, and check the import method for the actual class. I'm just thinking out loud, I don't know if it is feasible.
Absolutely lovely to have such a conversation! I will definitely take some time to read about the bridge in detail, but I think I got your point.
That's brilliant. You'll be happy to know the framework already supports exactly this:
LoggerInterface::class
rather than the concreteLogger::class
.I'm absolutely thrilled you brought this up, because it means the framework is on the right track. Thank you so much for your insights!
I'm building this project in the open, and I'd love to connect with other developers who are passionate about software architecture. If you have questions, ideas, or just want to say hi, please feel free to reach out in the comments or connect with me on LinkedIn. Your feedback and perspective are incredibly valuable as this project grows!
Wow, what a story, that “built something revolutionary and didn’t realize it” moment gives me chills.
What made you finally see it as “revolutionary” was there a specific user reaction or a tipping point in feedback?
This is wild...in the best way..
Thank you! This reaction really warms! :)
That moment was pretty wild for me too! What was the most exciting from your perspective?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.