Back

Loading…

Documentation

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)

  1. Create src/Client/Domain/Entity/Client.php (namespace App\Client\Domain\Entity)
  2. Create src/Client/Infrastructure/Persistence/ClientRepository.php
  3. Create src/Client/Application/Service/ClientService.php
  4. Create src/Client/UI/Controller/ClientController.php and UI/Form/ClientType.php
  5. Add Client: mapping block to doctrine.yaml
  6. Update 8 cross-module references (Site entity, SiteType form, SiteController, etc.)
  7. Remove App: mapping from doctrine.yaml once src/Entity/ is empty

Medium term

  • Move CreateAdminUserCommand, DeleteUserCommand, ListUsersCommand to User/Infrastructure/Console/
  • Move ProjectManagementController and SecurityAuditController to Site/UI/Controller/ or GitLab/UI/Controller/
  • Move DashboardService logic to per-module query services; keep a thin DashboardController
  • Move Pagination.php to src/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 ReviewUrlSource enum belongs in the Audit module or should move to Site
  • Consider whether DashboardController routes 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.

Contents