What you'll learn
Grow a PHP system without prematurely distributing it. Modular monoliths, clean dependency direction, domain modeling, commands, queries, and domain events create strong boundaries that can evolve before network boundaries become necessary.
By the end of this lesson, you'll be able to:
- Design a modular monolith
- Apply clean architecture dependency direction
- Use DDD building blocks and events selectively
Core mental model
Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.
| Concept | What it protects | Decision rule |
|---|---|---|
| Module | Change ownership boundary | Group a cohesive business capability with an explicit public API. |
| Application port | Dependency inversion | Core use cases define what infrastructure must provide. |
| Domain event | Cross-module fact | Publish a completed business fact after durable state change. |
Professional workflow
Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.
- Describe the system architecture boundary boundary: its inputs, outputs, invariants, and expected failures.
- Implement the smallest happy path behind an explicit contract.
- Add validation and translate low-level failures into language the caller understands.
- Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
- Refactor only after behavior is protected by a repeatable check.
Make the boundary visible
Guided code lab
Expose one module use case
The application service depends on module-owned ports; transport and PDO adapters remain outside.
<?php
namespace Academy\Enrollment\Application;
final readonly class EnrollStudent
{
public function __construct(
private Enrollments $enrollments,
private Courses $courses,
private EventPublisher $events,
) {}
public function handle(EnrollStudentCommand $command): void
{
$course = $this->courses->get($command->courseId);
$enrollment = Enrollment::begin($command->studentId, $course);
$this->enrollments->save($enrollment);
$this->events->publish(...$enrollment->releaseEvents());
}
}Make module rules executable
A dependency test prevents accidental imports from another module's internals.
Allowed:
Enrollment/Application -> Enrollment/Domain
Enrollment/Infrastructure -> Enrollment/Application
Catalog/Application -> SharedKernel
Forbidden:
Enrollment/Domain -> Laravel, PDO, HTTP
Catalog/* -> Enrollment/Infrastructure
SharedKernel -> any business moduleProduction practice
Contract
Modules own their data and public operations; cross-module access uses declared APIs or events, not table joins from anywhere.
Verification
Add architecture tests, module-level use-case tests, and contract tests for synchronous or event interfaces.
Operations
Trace cross-module workflows, version event schemas, and design repair paths for eventual consistency.
Common failure mode
Independent workshop
Re-architect the course platform as Catalog, Enrollment, Billing, and Identity modules with one end-to-end purchase flow.
Your finished workshop must include:
- Context map and dependency rules
- Module APIs plus event contracts
- Decision record comparing modular monolith and services
Definition of done
Recap & quick check
Key takeaways
- Architecture controls dependency direction.
- Modules align code with business capabilities.
- Domain models protect meaningful invariants.
- Distribution is an operational tradeoff, not an upgrade badge.
Quick check
1. Where should framework dependencies point?
2. What is a modular monolith?
3. When are microservices justified?
Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.