WatchTower — Modular Monolith Architecture Review
Date: 2026-05-27
Stack: Symfony 7.4.8 · Doctrine ORM 3.6 · PostgreSQL 16 · PHP 8.2+
Branch: init
1. What Was Done
The project completed an 8-phase refactoring from a flat Symfony structure (src/Entity/, src/Repository/, src/Service/, etc.) to a Modular Monolith with lightweight DDD and Hexagonal Architecture.
Each module now follows the Domain / Application / Infrastructure / UI layer convention:
| Layer | Responsibility |
|---|---|
| Domain | Entities, enums, domain events — pure business objects, no framework deps |
| Application | Use-case services, orchestration logic, ports (interfaces) |
| Infrastructure | Doctrine repos, Messenger handlers, HTTP clients, external APIs |
| UI | Symfony controllers, Twig forms |
2. Module Inventory
Seven bounded contexts were migrated across Phases 1–8:
AI (src/AI/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Agent/, Domain/Entity/, Domain/Enum/, Domain/Model/ |
| Application | Application/Agent/, Application/Service/ |
| Infrastructure | Infrastructure/DTO/, Infrastructure/Persistence/, Infrastructure/Prompt/, Infrastructure/Provider/ |
| UI | UI/Controller/, UI/Form/ |
Entities: AiAgentConfiguration, AiAgentUsage
Key services: AiAgentRunner, AiAgentUsageRecorder
Providers: OpenAI, Anthropic, Gemini (injected via services.yaml)
GitLab (src/GitLab/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Enum/ |
| Application | Application/Service/ |
| Infrastructure | Infrastructure/Api/, Infrastructure/Console/, Infrastructure/Exception/, Infrastructure/Messenger/ |
| UI | UI/Controller/ |
Key services: GitLabService, GitLabApiClient, SecurityArtifactViewService, GitLabActivityViewService
Async messages: RefreshGitLabCacheMessage → async
Report (src/Report/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Entity/, Domain/Enum/ |
| Application | Application/Service/ |
| Infrastructure | Infrastructure/Messenger/, Infrastructure/Pdf/, Infrastructure/Persistence/ |
| UI | UI/Controller/ |
Entities: Report, ApiResponseArchive
Async messages: GenerateReportPdfMessage → async
PDF backends: DomPDF (default) or shell command (configurable via APP_PDF_GENERATOR env)
Notification (src/Notification/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Entity/ |
| Application | Application/Service/ |
| Infrastructure | Infrastructure/Persistence/ |
| UI | UI/Controller/ |
Entities: Notification
Audit (src/Audit/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Entity/, Domain/Enum/, Domain/Event/ |
| Application | Application/AuditArtifact/, Application/Service/ |
| Infrastructure | Infrastructure/Automation/, Infrastructure/Messenger/, Infrastructure/Persistence/ |
| UI | UI/Controller/, UI/Form/ |
Entities: Audit, Finding, SiteAuditCheck, SiteUrlAuditResult
Enums: AuditStatus, FindingCategory, FindingSeverity, FindingSource, FindingStatus, RemediationEffort, ReviewUrlSource, ScrapeStatus
Async messages: RunAuditAutomationMessage → async
Automation: AutoFindingGenerator, TechnicalSeoFileChecker
Artifact parsers: ComposerAuditParser, PnpmAuditParser
Site (src/Site/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Entity/, Domain/Enum/ |
| Application | Application/Service/ |
| Infrastructure | Infrastructure/Automation/, Infrastructure/Messenger/, Infrastructure/Persistence/ |
| UI | UI/Controller/, UI/Form/ |
Entities: Site, SiteUrl, SiteRepository
Enum: ProjectStatus
Async messages: CrawlPageMessage → async
Automation services: ScraperService, HtmlExtractor, OnPageSeoAnalyzer, PageSpeedService, SitemapDiscoveryService, CrawlQueueManager
User (src/User/)
Layers: Domain · Application · Infrastructure · UI
| Sub-layer | Path |
|---|---|
| Domain | Domain/Entity/ |
| Application | Application/Service/ |
| Infrastructure | Infrastructure/Persistence/ |
| UI | UI/Controller/, UI/Form/ |
Entities: User (implements UserInterface, PasswordAuthenticatedUserInterface)
Forms: UserType, ProfileType, ChangePasswordType, RegistrationType
Controllers: AuthController, ProfileController, UserController
3. Infrastructure Configuration
Doctrine Mapping (config/packages/doctrine.yaml)
App → src/Entity (legacy — covers Client entity only)
AI → src/AI/Domain/Entity
Report → src/Report/Domain/Entity
Notification → src/Notification/Domain/Entity
Audit → src/Audit/Domain/Entity
Site → src/Site/Domain/Entity
User → src/User/Domain/Entity
14 mapped entities, all resolving correctly (doctrine:mapping:info reports 0 errors).
Messenger Routing (config/packages/messenger.yaml)
| Message | Transport |
|---|---|
Report\...\GenerateReportPdfMessage |
async |
Audit\...\RunAuditAutomationMessage |
async |
Site\...\CrawlPageMessage |
async |
GitLab\...\RefreshGitLabCacheMessage |
async |
All transports backed by Doctrine queue (MESSENGER_TRANSPORT_DSN). Failed messages go to a failed queue.
Security (config/packages/security.yaml)
password_hashers:
App\User\Domain\Entity\User: { algorithm: auto }
providers:
app_user_provider:
entity:
class: App\User\Domain\Entity\User
property: email
firewalls:
main:
provider: app_user_provider
user_checker: App\Security\UserChecker
form_login: { login_path: app_login, ... }
4. Cross-Module Dependency Map
User ←── AI (AiAgentConfiguration, AiAgentUsage reference User)
User ←── Audit (Audit references auditor: User)
User ←── Notification (Notification references user: User)
User ←── Report (Report references creator: User)
Site ←── Audit (SiteUrlAuditResult references SiteUrl; ReviewUrlSource is in Audit)
Site ←── GitLab (SiteRepository uses RepositoryProvider enum from GitLab)
Site ←── App\Entity\Client [legacy — not yet modularized]
Audit ←── Site (Audit.site references Site entity)
Audit ←── User (Audit.auditor references User)
Audit ←── Report (ApiResponseArchive references Audit)
Report ←── Audit (Report and ApiResponseArchive reference Audit entities)
Report ←── User
Notification ←── User
Observation: User is a central hub — depended on by 4 modules. This is normal for an auth-aware application.
Note on ReviewUrlSource: This enum lives in App\Audit\Domain\Enum but is used by SiteUrl (Site module). A future cleanup could move it to the Site module or a shared location.
5. What Remains in Legacy Locations
These files were intentionally left out of Phase 1–8 scope (no bounded context for them yet, or they span multiple concerns):
src/Entity/
| File | Note |
|---|---|
Client.php |
Doctrine entity — needs a Client module or absorbed into Site |
src/Repository/
| File | Note |
|---|---|
ClientRepository.php |
Follows Client entity |
src/Service/
| File | Note |
|---|---|
ClientService.php |
CRUD service for Client |
DashboardService.php |
Cross-module dashboard aggregation; large file (~20KB) |
src/Controller/
| File | Note |
|---|---|
ClientController.php |
Standard CRUD for Client |
DashboardController.php |
Main dashboard — depends on DashboardService |
HomeController.php |
Landing/home page |
ProjectManagementController.php |
Cross-cutting project view |
SecurityAuditController.php |
GitLab security panel |
src/Form/
| File | Note |
|---|---|
ClientType.php |
Follows Client entity |
src/Command/
| File | Note |
|---|---|
CreateAdminUserCommand.php |
Could move to User/Infrastructure/Console/ |
DeleteUserCommand.php |
Same |
ListUsersCommand.php |
Same |
ListProjectsCommand.php |
Cross-module, reasonable to leave or add to Site |
src/Security/
| File | Note |
|---|---|
UserChecker.php |
Uses App\User\Domain\Entity\User; reasonable to keep in Security namespace |
src/Pagination/
| File | Note |
|---|---|
Pagination.php |
Generic utility — candidate for a future Shared module |
6. Test Coverage Status
11 test files exist, all in a flat structure under tests/:
| Test | Covers |
|---|---|
Command/ListProjectsCommandTest.php |
ListProjectsCommand |
Command/ImportGitLabAuditArtifactCommandTest.php |
GitLab import command |
Service/SecurityArtifactViewServiceTest.php |
GitLab security view |
Service/GitLabActivityViewServiceTest.php |
GitLab activity |
Service/PdfGeneratorsTest.php |
Report PDF backends |
Service/ReportPdfTemplateTest.php |
Report PDF template |
Service/ReportServicePdfTest.php |
ReportService PDF generation |
Service/AuditArtifact/ComposerAuditParserTest.php |
Composer vulnerability parser |
Service/AuditArtifact/AuditArtifactImportServiceTest.php |
Artifact import |
Service/AuditArtifact/PnpmAuditParserTest.php |
pnpm vulnerability parser |
Gaps:
- No tests for Domain entities or business rules (AuditService, FindingService, etc.)
- No tests for Site automation (ScraperService, PageSpeedService)
- No integration tests for the Messenger handlers
- Test directory mirrors old flat structure — could be reorganized to mirror module layout
7. Known Issues Fixed During Review
| Issue | Fix |
|---|---|
config/packages/security.yaml entity provider had class: App\Entity\User (old namespace) while the entity moved to App\User\Domain\Entity\User |
Fixed in commit 0a9439e — authentication would have failed without this |
8. Suggested Next Steps
Immediate (before next deployment)
- Manual smoke test of login / logout / register flows to confirm authentication works end-to-end with new User entity path.
Short term — Client module (Phase 9)
- Create
src/Client/Domain/Entity/Client.php(namespaceApp\Client\Domain\Entity) - Create
src/Client/Infrastructure/Persistence/ClientRepository.php - Create
src/Client/Application/Service/ClientService.php - Create
src/Client/UI/Controller/ClientController.phpandUI/Form/ClientType.php - Add
Client:mapping block todoctrine.yaml - Update 8 cross-module references (Site entity, SiteType form, SiteController, etc.)
- Remove
App:mapping fromdoctrine.yamloncesrc/Entity/is empty
Medium term
- Move
CreateAdminUserCommand,DeleteUserCommand,ListUsersCommandtoUser/Infrastructure/Console/ - Move
ProjectManagementControllerandSecurityAuditControllertoSite/UI/Controller/orGitLab/UI/Controller/ - Move
DashboardServicelogic to per-module query services; keep a thinDashboardController - Move
Pagination.phptosrc/Shared/if other shared utilities accumulate - Reorganize
tests/to mirror module layout:tests/Audit/,tests/Site/, etc. - Add domain-layer unit tests for
AuditService,FindingService,CrawlQueueManager
Low priority
- Evaluate whether
ReviewUrlSourceenum belongs in the Audit module or should move to Site - Consider whether
DashboardControllerroutes should have a dedicated bounded context
9. Appendix — File Counts by Module
src/AI/ ~29 files
src/Audit/ ~35 files
src/GitLab/ ~16 files
src/Notification/ ~5 files
src/Report/ ~14 files
src/Site/ ~23 files
src/User/ ~11 files
──────────────────────────
Total modular ~133 files
Legacy src/ ~17 files remaining
Review conducted on 2026-05-27 after completing Phases 1–8 of the modular monolith refactoring.