A Secured robust, event-driven payment microservices system API key middleware for resource authorization and tenant isolation. Built with a modular, event-driven architecture for scalability and maintainability.
This application follows three fundamental architectural patterns:
- Module-Based Architecture - Self-contained, auto-discovered modules
- Contract-Based Dependency Injection - Interface-driven service binding
- Event-Driven Design - Decoupled, configurable event processing
Modules are automatically discovered from the src/Modules/ directory. Each module is self-contained with its own:
- Service providers
- Database migrations
- Routes
- Controllers, Services, Repositories
- Configuration files
- Event listeners
Module Structure:
src/Modules/{ModuleName}/
├── Contracts/ # Service interfaces
├── Database/
│ ├── Migrations/ # Module migrations
│ ├── Seeders/ # Module seeders
│ └── Models/ # Eloquent models
├── Http/
│ ├── Controllers/ # API controllers
│ ├── Requests/ # Form validation
│ └── Resources/ # API resources
├── Providers/ # Service providers
├── Repositories/ # Data access layer
├── Services/ # Business logic
├── Events/ # Domain events
├── Listeners/ # Event listeners
├── routes/
│ └── api.php # Module routes
└── config/ # Module configuration
The ModuleManager automatically discovers and registers modules:
$moduleManager = app(ModuleManager::class);
$modules = $moduleManager->getModules();
$health = $moduleManager->getModuleHealth('User');Features:
- Auto-discovery of modules in
src/Modules/ - Service provider registration with priority support
- Health status monitoring
- Route and migration collection
- Module configuration loading
Each module extends BaseModuleServiceProvider which provides:
- Automatic service registration
- Module namespace resolution
- Configuration merging
- Policy registration
Example:
class UserServiceProvider extends BaseModuleServiceProvider
{
protected string $moduleNamespace = 'App\\Modules\\User';
protected array $configFiles = ['user'];
protected function registerServices(): void
{
// Bind interfaces to implementations
$this->app->bind(UserServiceInterface::class, UserService::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}
}All services communicate through interfaces (contracts), enabling:
- Easy testing with mocks
- Implementation swapping
- Clear service boundaries
- Dependency inversion
Services are bound to their interfaces in module service providers:
// In UserServiceProvider
protected function registerServices(): void
{
// Singleton binding
$this->app->singleton(UserService::class);
// Interface binding
$this->app->bind(UserServiceInterface::class, UserService::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}Controllers and services receive dependencies via constructor injection:
class UserController extends Controller
{
public function __construct(
private UserServiceInterface $userService
) {}
}The SubModuleServiceRegistry automatically discovers and registers services that implement SubModuleServiceContract:
// Services implementing SubModuleServiceContract are auto-registered
class UserBalanceService implements SubModuleServiceContract
{
public function getDefaultSubModuleName(): string
{
return 'user_balance';
}
}
// Access via registry
$registry = app(SubModuleServiceRegistry::class);
$service = $registry->get('user_balance');The application uses a configurable event system that supports three processing modes:
- Sync - Immediate processing
- Queue - Background job processing
- Scheduled - Batch processing via scheduled events table
Events are configured in config/events.php:
'events' => [
'investment_created' => [
'class' => InvestmentWasCreated::class,
'mode' => env('EVENT_INVESTMENT_MODE', 'queue'),
'queue' => env('EVENT_INVESTMENT_QUEUE', 'default'),
'priority' => 'high',
'listeners' => [
'create_transaction' => [
'class' => CreateTransactionForEntity::class,
'mode' => 'queue',
'tries' => 5,
'backoff' => [30, 60, 120],
],
],
],
],All notification events implement NotificationEventsContract:
interface NotificationEventsContract
{
public function getEntity();
public function getNotifiable();
public function getEventType(): string;
public function getChannels(): array;
public function getTitle(): string;
public function getMessage(): string;
// ... more methods
}Events extend BaseNotificationEvent:
class UserWasCreatedEvent extends BaseNotificationEvent
{
public function __construct(public User $user) {}
public function getEventType(): string
{
return 'user_was_created';
}
public function getEntity()
{
return $this->user;
}
public function getNotifiable()
{
return $this->user;
}
public function getChannels(): array
{
return ['database', 'mail'];
}
}Listeners implement ConfigurableListenerInterface to read configuration:
class SendEntityNotification implements ShouldQueue, ConfigurableListenerInterface
{
use ConfigurableListener, InteractsWithQueue;
public function handle(NotificationEventsContract $event): void
{
// Listener reads its configuration from config/events.php
// Queue, retries, backoff all configured per listener
}
}The EventDispatcher service routes events based on configuration:
$eventDispatcher->dispatch(new InvestmentWasCreated($investment), 'investment_created');The dispatcher checks config/events.php to determine:
- Processing mode (sync/queue/scheduled)
- Queue name
- Retry configuration
- Priority
The notification system uses an outbox pattern to ensure reliable notification delivery:
- Event Fired → Listener publishes to
notification_outboxtable - Outbox Processing → Scheduled command processes pending notifications
- Notification Delivery → Creates database notifications and queues emails
Event → Listener → NotificationOutboxPublisher → notification_outbox table
↓
ProcessNotificationOutbox Command
↓
Database Notification + Queued Email
The notification_outbox table stores pending notifications:
event_type- Type of event (e.g., 'user_was_created')notifiable_type/notifiable_id- Who receives the notificationentity_type/entity_id- What triggered the notificationchannels- Delivery channels (database, mail, sms)payload- Notification datastatus- pending|processing|sent|faileddedupe_key- Prevents duplicate notifications
The notifications:outbox:process command runs via Laravel scheduler:
// routes/console.php
Schedule::command('notifications:outbox:process --limit=100')
->everyMinute()
->withoutOverlapping()
->runInBackground();Setup:
# On server, add to crontab:
* * * * * cd /var/www/trader-apis && docker exec trader-apis-app php artisan schedule:runphp artisan module:list
php artisan module:list --healthphp artisan module:create Product --description="Product management"php artisan module:migrate User
php artisan module:migrate --all
php artisan module:rollback User
php artisan module:migration:statusphp artisan module:cache:providers
php artisan module:providers:listGET /api/gateway/status- Gateway statusGET /api/gateway/health- Overall health checkGET /api/gateway/modules- All modules infoGET /api/gateway/modules/{module}- Specific module info
Each module defines routes in routes/api.php:
Route::prefix('api/v1/users')->group(function () {
Route::get('/', [UserController::class, 'index']);
Route::post('/', [UserController::class, 'store']);
});Each module has its own migrations in Database/Migrations/. Migrations are automatically discovered and registered.
Polymorphic relationships use clean aliases via morph maps:
// config/core.php
'morph_maps' => [
'user' => \App\Modules\User\Database\Models\User::class,
'investment' => \App\Modules\Investment\Database\Models\Investment::class,
]Modules can use separate database connections via configuration:
// Module config
'database' => [
'connection' => env('USER_DB_CONNECTION', 'default'),
'prefix' => env('USER_DB_PREFIX', ''),
],- Auth - Authentication and authorization
- User - User management
- Client - Multi-client support
- Investment - Investment management
- Transaction - Transaction processing
- Payment - Payment processing
- Funding - Account funding
- Withdrawal - Withdrawal processing
- Balance - Balance management
- Notification - Notification system
- Market - Market data
- Pricing - Pricing engine
- Currency - Currency management
- Category - Transaction categories
- Role - Role-based access control
- Dashboard - Dashboard data
- Swap - Currency swapping
Modules are automatically discovered and registered on application boot.
All services use interfaces, enabling easy testing and implementation swapping.
Events can be processed sync, queued, or scheduled based on configuration.
Reliable notification delivery using the outbox pattern with scheduled processing.
Automatic registration of services implementing SubModuleServiceContract.
Module health checks via API Gateway endpoints.
Isolated migrations per module with rollback support.
- Generate module structure:
php artisan module:create Product- Define contracts in
Contracts/:
interface ProductServiceInterface {}- Implement services:
class ProductService implements ProductServiceInterface {}- Bind in service provider:
$this->app->bind(ProductServiceInterface::class, ProductService::class);- Create events:
class ProductWasCreated extends BaseNotificationEvent {}-
Register listeners in event service provider
-
Add routes in
routes/api.php -
Create migrations:
php artisan module:make:migration Product create_products_table- Keep modules focused on a single domain
- Use contracts for all service interfaces
- Minimize cross-module dependencies
- Each module should be independently testable
- Events should represent domain events (things that happened)
- Use descriptive event names (e.g.,
UserWasCreated,InvestmentWasCreated) - Events should contain all data needed by listeners
- Store IDs, not full models, for serialization safety
- Services should implement interfaces
- Use dependency injection, not facades
- Keep services focused on business logic
- Repositories handle data access
- Mock interfaces, not concrete classes
- Test modules in isolation
- Use contracts for test doubles
- Test event listeners separately
Each module can define config/{module}.php:
return [
'module' => [
'name' => 'User',
'version' => '1.0.0',
],
'database' => [
'connection' => env('USER_DB_CONNECTION', 'default'),
],
];Events are configured in config/events.php with per-event and per-listener settings.
Notification channels and providers configured in config/notification.php.
This architecture provides a solid foundation for building scalable, maintainable applications with clear module boundaries and event-driven communication.