Modular monolith building blocks: module contracts, integration events, Anti-Corruption Layer, and module registry.
Build isolated modules that communicate through contracts, not concrete dependencies.
composer require solidframe/modularuse SolidFrame\Modular\Module\AbstractModule;
final class OrderModule extends AbstractModule
{
public function __construct()
{
parent::__construct(
name: 'order',
dependsOn: ['inventory', 'payment'],
);
}
}use SolidFrame\Modular\Registry\InMemoryModuleRegistry;
$registry = new InMemoryModuleRegistry();
$registry->register(new OrderModule());
$registry->register(new InventoryModule());
$registry->register(new PaymentModule());
// List all modules
$modules = $registry->all();
// Get by name
$order = $registry->get('order');
$order->dependsOn(); // ['inventory', 'payment']
// Topological sort — respects dependency order
$sorted = $registry->dependencyOrder();
// [InventoryModule, PaymentModule, OrderModule]Circular dependencies are detected automatically:
use SolidFrame\Modular\Exception\CircularDependencyException;
// A depends on B, B depends on A → throws CircularDependencyExceptionDefine what a module exposes to others via ModuleContractInterface:
use SolidFrame\Modular\Contract\ModuleContractInterface;
interface InventoryContractInterface extends ModuleContractInterface
{
public function reserve(string $productId, int $quantity): void;
public function checkAvailability(string $productId): int;
}Other modules depend on the contract, never on the implementation:
final readonly class PlaceOrderHandler implements CommandHandler
{
public function __construct(
private InventoryContractInterface $inventory,
) {}
public function __invoke(PlaceOrder $command): void
{
$this->inventory->reserve($command->productId, $command->quantity);
// ...
}
}Modules communicate asynchronously via integration events:
use SolidFrame\Modular\Event\AbstractIntegrationEvent;
final readonly class OrderPlacedIntegration extends AbstractIntegrationEvent
{
public function __construct(
public string $orderId,
public string $productId,
public int $quantity,
) {
parent::__construct(sourceModule: 'order');
}
public function eventName(): string
{
return 'order.placed';
}
}
// In another module's listener:
final readonly class ReserveInventoryOnOrderPlaced implements EventListener
{
public function __invoke(OrderPlacedIntegration $event): void
{
$event->sourceModule(); // 'order'
$event->occurredAt(); // DateTimeImmutable
// reserve inventory...
}
}Translate between module boundaries with TranslatorInterface:
use SolidFrame\Modular\AntiCorruption\TranslatorInterface;
/** @implements TranslatorInterface<ExternalOrder, DomainOrder> */
final readonly class OrderTranslator implements TranslatorInterface
{
public function translate(object $source): object
{
return new DomainOrder(
id: new OrderId($source->getId()),
total: Money::from($source->getTotal(), $source->getCurrency()),
);
}
}| Class / Interface | Purpose |
|---|---|
ModuleInterface |
Contract for module definition |
AbstractModule |
Base module with name and dependencies |
ModuleRegistryInterface |
Module registration and lookup |
InMemoryModuleRegistry |
In-memory registry with topological sort |
ModuleContractInterface |
Marker for module public APIs |
IntegrationEventInterface |
Cross-module event contract |
AbstractIntegrationEvent |
Base integration event |
TranslatorInterface |
Anti-Corruption Layer translator |
ModuleNotFoundException |
Module not found in registry |
CircularDependencyException |
Circular module dependency detected |
- solidframe/core — DomainEventInterface, Bus interfaces
- solidframe/event-driven — EventBus for integration events
- solidframe/cqrs — Command/Query handling within modules
- solidframe/archtest — Enforce module isolation rules
- solidframe/laravel — ModuleServiceProvider, auto-discovery,
make:module,solidframe:module:list - solidframe/symfony — Module discovery,
make:module,solidframe:module:list
This repository is a read-only split of the solidframe/solidframe monorepo, auto-synced on every push to main. Issues, pull requests, and discussions belong in the monorepo.