Files
michaelschiemer/docs/ARCHITECTURE-DECISION.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

33 KiB
Raw Blame History

Architecture Decision: Layer-Based vs Module-Based Organization

Decision Date: 2025-01-28 Status: Recommended Deciders: Development Team Related Documents:

  • SRC-ARCHITECTURE-ANALYSIS.md - Current architecture analysis
  • DOCUMENTATION-ANALYSIS.md - Documentation structure analysis

Executive Summary

Decision: Maintain 4-layer Clean Architecture with enhanced modular organization within each layer (Hybrid Approach)

Rationale:

  • Framework layer constitutes 85.3% of codebase (3,883 files)
  • Pure module-based approach would duplicate this massive framework across modules
  • Hybrid approach provides modular benefits while preserving shared framework infrastructure
  • Estimated effort: 2-3 months (vs 6-12 months for pure module approach)

Impact:

  • Maintains Clean Architecture compliance
  • Achieves Domain-Driven Design benefits
  • Avoids massive code duplication
  • Minimizes refactoring effort
  • Preserves existing framework investments

Context & Problem Statement

Current Architecture State

Analyzed Structure (from SRC-ARCHITECTURE-ANALYSIS.md):

Total Files: 4,551 PHP files
Total Directories: 1,052

Layer Distribution:
├── Framework:      3,883 files (85.3%) - TOO LARGE
├── Application:      420 files (9.2%)  - Below target
├── Domain:           184 files (4.0%)  - TOO THIN
└── Infrastructure:    64 files (1.5%)  - Below target

Current Grade: C+ (75%) - "Good foundation with critical architectural flaws"

Identified Problems

3 Critical Architectural Violations:

  1. HTTP Layer in Domain (src/Domain/Meta/Http/)

    • Controllers with #[Route] attributes in Domain layer
    • 406 lines of HTTP-specific code misplaced
  2. DI Container Setup in Domain (5 domains affected)

    • Framework-specific dependency injection in Domain
    • Violates Dependency Rule (Domain depends on Framework)
  3. Infrastructure Nested in Application (src/Application/Website/Infrastructure/GeoIp/)

    • Wrong dependency direction
    • Confusing architecture

Strategic Question

"Should we maintain 4-layer architecture or switch to pure module-based organization?"

This decision has fundamental implications:

  • Code organization strategy
  • Framework reusability
  • Refactoring effort (months vs years)
  • Team cognitive load
  • Maintainability long-term

Decision Drivers

Key Factors Influencing Decision

  1. Framework Size: 85.3% of codebase is framework infrastructure
  2. Code Duplication Risk: Pure modules would replicate framework per module
  3. Clean Architecture Benefits: Dependency inversion, testability, maintainability
  4. Domain-Driven Design Goals: Bounded contexts, domain focus
  5. Refactoring Effort: Time and risk considerations
  6. Team Familiarity: Existing knowledge of current structure
  7. Framework Reusability: Shared infrastructure across all modules

Stakeholder Concerns

  • Development Team: Minimize disruption, preserve framework investments
  • Architecture: Maintain Clean Architecture principles
  • Business: Minimize refactoring time, maintain velocity
  • Quality: Improve testability, reduce coupling

Considered Options

Option 1: Pure Module-Based (Vertical Slicing)

Structure:

src/
├── User/                    # Self-contained module
│   ├── Domain/
│   ├── Application/
│   ├── Infrastructure/
│   └── Framework/          # Duplicated framework code!
├── Order/                   # Self-contained module
│   ├── Domain/
│   ├── Application/
│   ├── Infrastructure/
│   └── Framework/          # Duplicated framework code!
└── Product/                 # Self-contained module
    ├── Domain/
    ├── Application/
    ├── Infrastructure/
    └── Framework/          # Duplicated framework code!

Advantages:

  • True bounded contexts - modules are completely independent
  • Team autonomy - each module owned by dedicated team
  • Deploy modules independently (microservices-ready)
  • Pure Domain-Driven Design implementation
  • No cross-module dependencies

Disadvantages:

  • CRITICAL: Framework duplication (85.3% × modules = massive overhead)
  • Framework updates must propagate to ALL modules
  • Code duplication: HTTP layer, Database, Cache, Queue, Events
  • Increased maintenance burden (N frameworks to update)
  • Inconsistency risk across modules
  • Massive refactoring effort (6-12 months)
  • Loss of framework investment (3,883 files built over time)

Estimated Effort: 6-12 months Risk Level: HIGH


Option 2: Hybrid Approach (Layers + Modules)

Recommended Structure:

src/
├── Domain/                  (20-30% - business logic)
│   ├── User/               (Module - bounded context)
│   │   ├── Entity/
│   │   ├── ValueObject/
│   │   ├── Event/
│   │   ├── Repository/     (Interfaces only!)
│   │   └── Service/
│   ├── Order/              (Module - bounded context)
│   │   ├── Entity/
│   │   ├── ValueObject/
│   │   ├── Event/
│   │   ├── Repository/
│   │   └── Service/
│   └── Product/            (Module - bounded context)
│
├── Application/             (25-35% - use cases)
│   ├── User/               (Module - aligned with domain)
│   │   ├── Controller/
│   │   ├── Command/
│   │   └── Query/
│   ├── Order/              (Module - aligned with domain)
│   │   ├── Controller/
│   │   ├── Command/
│   │   └── Query/
│   └── Admin/              (Feature module - cross-cutting)
│       ├── Analytics/
│       ├── Content/
│       └── System/
│
├── Framework/               (40-50% - SHARED infrastructure)
│   ├── Core/               # Shared by ALL modules
│   ├── Database/           # Shared by ALL modules
│   ├── Http/               # Shared by ALL modules
│   ├── Cache/              # Shared by ALL modules
│   ├── Queue/              # Shared by ALL modules
│   ├── Events/             # Shared by ALL modules
│   └── [32 more shared components]
│
└── Infrastructure/          (5-10% - external integrations)
    └── ServiceProviders/   # Module-specific DI setup
        ├── UserServiceProvider.php
        ├── OrderServiceProvider.php
        └── ProductServiceProvider.php

Advantages:

  • Clean Architecture compliance maintained
  • Framework shared across all modules (no duplication!)
  • Modular organization where beneficial (Domain, Application)
  • Domain-Driven Design benefits (bounded contexts)
  • Reduced refactoring effort (2-3 months)
  • Framework investment preserved
  • Single framework to maintain and update
  • Consistency across modules (shared patterns)
  • Testability (clear boundaries, mocking framework components)

Disadvantages:

  • ⚠️ Framework is shared dependency (not pure bounded contexts)
  • ⚠️ Module deployment still coupled via framework
  • ⚠️ Requires discipline to avoid cross-domain dependencies

Estimated Effort: 2-3 months Risk Level: LOW-MEDIUM


Option 3: Keep Current Structure (Do Nothing)

Structure: Current 4-layer architecture without modular organization

Advantages:

  • Zero refactoring effort
  • No disruption to development

Disadvantages:

  • Architectural violations remain (HTTP in Domain, DI in Domain)
  • Poor domain organization (Domain layer too thin at 4%)
  • Framework bloat (85.3% - too large)
  • Technical debt accumulation
  • Reduced maintainability over time

Estimated Effort: 0 months (no change) Risk Level: HIGH (accumulating technical debt)


Decision Outcome

Chosen Option: Option 2 - Hybrid Approach (Layers + Modules)

Rationale:

  1. Framework Reusability: 85.3% of codebase is framework infrastructure that SHOULD be shared

    • Database layer (387 files) - shared across all modules
    • HTTP layer (156 files) - shared routing, middleware
    • Queue system (89 files) - shared background processing
    • Cache layer (67 files) - shared caching infrastructure
    • 40 more framework components - all reusable
  2. Clean Architecture Preserved: Maintains dependency rule while adding modularity

    • Domain depends on nothing
    • Application depends on Domain
    • Framework provides infrastructure
    • Infrastructure implements interfaces
  3. Domain-Driven Design Benefits: Modules represent bounded contexts

    • Domain/User/ - user bounded context
    • Domain/Order/ - order bounded context
    • Domain/Product/ - product bounded context
    • Clear boundaries, no cross-domain dependencies
  4. Realistic Refactoring Timeline: 2-3 months vs 6-12 months

    • Phase 1: Priority 1 Fixes (2-3 days)
    • Phase 2: Domain Layer Modularization (1-2 weeks)
    • Phase 3: Application Layer Modularization (1-2 weeks)
    • Phase 4: Infrastructure Consolidation (1-2 weeks)
  5. Risk Mitigation: Incremental changes, testable at each phase


Implementation Strategy

Phase 1: Priority 1 Fixes (2-3 days) - CRITICAL

Objective: Fix architectural violations

Tasks:

  1. Move HTTP out of Domain

    # Before: src/Domain/Meta/Http/Controller/MetaAdminController.php
    # After:  src/Application/Admin/Controllers/Meta/MetaAdminController.php
    
    mv src/Domain/Meta/Http/Controller/ src/Application/Admin/Controllers/Meta/
    mv src/Domain/Meta/Http/Middleware/ src/Framework/Http/Middlewares/
    
  2. Move DI to Infrastructure

    mkdir -p src/Infrastructure/ServiceProviders
    
    # Transform Initializers to ServiceProviders
    mv src/Domain/Asset/DI/AssetServiceInitializer.php \
       src/Infrastructure/ServiceProviders/AssetServiceProvider.php
    
    mv src/Domain/Cms/DI/CmsServiceInitializer.php \
       src/Infrastructure/ServiceProviders/CmsServiceProvider.php
    
    # Repeat for User, Order, Console domains
    
  3. Fix Infrastructure Nesting

    mv src/Application/Website/Infrastructure/GeoIp/ \
       src/Infrastructure/GeoIp/
    
  4. Update Namespaces and Imports

    • Run PSR-4 compliance check
    • Update all references to moved files
    • Test all affected functionality

Success Criteria:

  • Zero architectural violations
  • All tests pass
  • PSR-4 compliance: 100%

Code Example - ServiceProvider Transformation:

// Before: src/Domain/Asset/DI/AssetServiceInitializer.php
namespace App\Domain\Asset\DI;

use App\Framework\DI\Container;
use App\Framework\DI\Initializer;

final readonly class AssetServiceInitializer
{
    #[Initializer]
    public function __invoke(Container $container): void
    {
        $container->singleton(AssetRepository::class, ...);
        $container->singleton(AssetService::class, ...);
    }
}

// After: src/Infrastructure/ServiceProviders/AssetServiceProvider.php
namespace App\Infrastructure\ServiceProviders;

use App\Framework\DI\Container;
use App\Framework\DI\ServiceProvider;

final readonly class AssetServiceProvider implements ServiceProvider
{
    public function register(Container $container): void
    {
        $container->singleton(AssetRepository::class, ...);
        $container->singleton(AssetService::class, ...);
    }
}

Phase 2: Domain Layer Modularization (1-2 weeks)

Objective: Organize Domain layer by bounded contexts

Current State:

src/Domain/
├── User/         (23 files) - mixed structure
├── Asset/        (18 files) - mixed structure
├── Cms/          (15 files) - mixed structure
├── Order/        (29 files) - mixed structure
└── Product/      (12 files) - mixed structure

Target State:

src/Domain/
├── User/                    # User bounded context
│   ├── Entity/
│   │   └── User.php
│   ├── ValueObject/
│   │   ├── UserId.php
│   │   ├── Email.php
│   │   └── UserName.php
│   ├── Event/
│   │   ├── UserRegisteredEvent.php
│   │   └── UserUpdatedEvent.php
│   ├── Repository/
│   │   └── UserRepositoryInterface.php  # Interface only!
│   └── Service/
│       └── UserService.php
│
├── Order/                   # Order bounded context
│   ├── Entity/
│   │   ├── Order.php
│   │   └── OrderItem.php
│   ├── ValueObject/
│   │   ├── OrderId.php
│   │   ├── OrderStatus.php
│   │   └── Money.php
│   ├── Event/
│   │   ├── OrderCreatedEvent.php
│   │   └── OrderCompletedEvent.php
│   ├── Repository/
│   │   └── OrderRepositoryInterface.php
│   └── Service/
│       └── OrderService.php
│
└── Product/                 # Product bounded context
    ├── Entity/
    ├── ValueObject/
    ├── Event/
    ├── Repository/
    └── Service/

Benefits:

  • Clear bounded contexts
  • Self-documenting structure
  • Easy to locate domain logic
  • No cross-domain dependencies
  • Repository interfaces (implementations in Infrastructure)

Phase 3: Application Layer Modularization (1-2 weeks)

Objective: Align Application layer with Domain modules

Current State:

src/Application/
├── Website/       (89 files) - feature-based
├── Admin/         (234 files) - feature-based
├── Api/           (67 files) - feature-based
├── Contact/       (8 files) - feature-based
└── Console/       (22 files) - feature-based

Target State:

src/Application/
├── User/                    # Aligned with Domain/User
│   ├── Controller/
│   │   └── UserController.php
│   ├── Command/
│   │   ├── CreateUserCommand.php
│   │   └── CreateUserHandler.php
│   └── Query/
│       ├── GetUserQuery.php
│       └── GetUserHandler.php
│
├── Order/                   # Aligned with Domain/Order
│   ├── Controller/
│   ├── Command/
│   └── Query/
│
├── Product/                 # Aligned with Domain/Product
│   ├── Controller/
│   ├── Command/
│   └── Query/
│
└── Admin/                   # Cross-cutting feature module
    ├── Analytics/
    ├── Content/
    ├── Infrastructure/
    ├── Notifications/
    └── System/

Benefits:

  • Clear mapping: Application/User → Domain/User
  • CQRS patterns (Command/Query separation)
  • Feature-based organization where appropriate (Admin)
  • Reduced cognitive load

Phase 4: Infrastructure Consolidation (1-2 weeks)

Objective: Centralize infrastructure implementations

Target State:

src/Infrastructure/
├── ServiceProviders/        # DI setup per module
│   ├── UserServiceProvider.php
│   ├── OrderServiceProvider.php
│   └── ProductServiceProvider.php
│
├── Persistence/             # Database implementations
│   ├── User/
│   │   └── DatabaseUserRepository.php    # Implements Domain/User/Repository/UserRepositoryInterface
│   ├── Order/
│   │   └── DatabaseOrderRepository.php
│   └── Product/
│       └── DatabaseProductRepository.php
│
├── GeoIp/                   # External service integrations
│   ├── GeoIpService.php
│   └── MaxMindProvider.php
│
└── Email/                   # External service integrations
    ├── EmailService.php
    └── SmtpProvider.php

Benefits:

  • Infrastructure implementations separated from Domain interfaces
  • Clear dependency inversion (Domain → Interface ← Infrastructure)
  • Easy to swap implementations (testing, different databases)
  • ServiceProviders wire up dependencies

Consequences

Positive Consequences

  1. Clean Architecture Compliance

    • All layers respect Dependency Rule
    • Domain layer pure (no framework dependencies)
    • Testability improved (mock infrastructure easily)
  2. Domain-Driven Design Benefits

    • Clear bounded contexts (User, Order, Product)
    • Domain logic focused and cohesive
    • Business rules visible and maintainable
  3. Framework Preservation

    • No code duplication (Framework shared)
    • Single framework to maintain
    • Consistency across all modules
    • Framework investment preserved (3,883 files)
  4. Maintainability

    • Clear structure reduces cognitive load
    • Easy to locate code (Domain/User/Entity/User.php)
    • Self-documenting architecture
    • Onboarding new developers easier
  5. Realistic Timeline

    • 2-3 months total effort
    • Incremental changes (low risk)
    • Testable at each phase

Negative Consequences (Mitigated)

  1. Framework is Shared Dependency

    • Mitigation: Framework is stable, well-tested infrastructure
    • Trade-off: Accepting this trade-off for massive code reuse benefit
  2. Module Deployment Coupling

    • Mitigation: Not pursuing microservices architecture (monolith is appropriate)
    • Trade-off: Deployment simplicity vs independent module deployment
  3. Discipline Required

    • Mitigation: Code review process, automated PSR-4 checks, architectural tests
    • Trade-off: Team training vs long-term maintainability

Comparison Matrix

Aspect Pure Modules Hybrid (Recommended) Keep Current
Clean Architecture Yes Yes Violations exist
DDD Bounded Contexts Pure Practical Weak
Framework Duplication Massive (85.3% × N) None (shared) None
Code Reuse Low (per-module) High (shared) High
Refactoring Effort 6-12 months 2-3 months 0 months
Risk Level HIGH LOW-MEDIUM HIGH (debt)
Team Autonomy Full (per module) ⚠️ Partial None
Deploy Independence Yes (microservices) No (monolith) No
Maintenance Burden High (N frameworks) Low (1 framework) ⚠️ Medium
Architectural Quality A+ (pure DDD) A (pragmatic) C+ (violations)

Scoring:

  • Pure Modules: 5/10 (ideal theory, impractical for this project)
  • Hybrid: 9/10 (recommended - pragmatic and effective)
  • Keep Current: 3/10 (accumulating technical debt)

Migration Risks & Mitigation

Risk 1: Breaking Changes During Refactoring

Probability: MEDIUM Impact: HIGH

Mitigation:

  • Comprehensive test suite BEFORE starting
  • Feature flags for gradual rollout
  • Incremental changes with continuous testing
  • Rollback plan for each phase

Risk 2: Team Resistance to Change

Probability: LOW Impact: MEDIUM

Mitigation:

  • Document rationale (this document)
  • Training sessions on new structure
  • Gradual transition (4 phases over 2-3 months)
  • Team involvement in decision-making

Risk 3: Overlooked Dependencies

Probability: MEDIUM Impact: MEDIUM

Mitigation:

  • Automated dependency analysis
  • PSR-4 compliance checks
  • Static analysis tools (PHPStan)
  • Code review process for all moves

Risk 4: Timeline Overrun

Probability: LOW Impact: MEDIUM

Mitigation:

  • Conservative time estimates (2-3 months)
  • Phase-based approach (can pause between phases)
  • Automated tooling for bulk operations
  • Dedicated refactoring time (not mixed with feature work)

Success Metrics

Phase 1 Completion (Priority 1 Fixes)

  • Zero architectural violations
  • All tests pass (100%)
  • PSR-4 compliance: 100%
  • No HTTP code in Domain layer
  • No DI code in Domain layer
  • Infrastructure at top level (not nested)

Phase 2-4 Completion (Full Migration)

  • Domain layer: 15-20% of codebase (from 4%)
  • Framework layer: 40-50% of codebase (from 85.3%)
  • Clear bounded contexts in Domain layer
  • Application layer aligned with Domain modules
  • Infrastructure ServiceProviders per module
  • Overall Architecture Grade: A- (90%) (from C+ 75%)

Long-Term Success

  • Reduced time to locate code (faster development)
  • Easier onboarding (clear structure)
  • Fewer architectural violations (ongoing)
  • Higher test coverage (better boundaries)
  • Improved maintainability (team feedback)

Code Examples

Example 1: Domain Module (User Bounded Context)

Before Migration:

src/Domain/User/
├── User.php                           # Mixed: Entity + logic
├── UserRepository.php                 # Confusion: Interface or implementation?
├── UserService.php                    # Service
└── DI/
    └── UserServiceInitializer.php    # WRONG: DI in Domain!

After Migration:

src/Domain/User/
├── Entity/
│   └── User.php                       # Pure entity
├── ValueObject/
│   ├── UserId.php                     # Type-safe ID
│   ├── Email.php                      # Validated email
│   └── UserName.php                   # Validated name
├── Event/
│   └── UserRegisteredEvent.php       # Domain event
├── Repository/
│   └── UserRepositoryInterface.php   # Interface only!
└── Service/
    └── UserService.php                # Domain service

Domain Entity (Pure business logic):

// src/Domain/User/Entity/User.php
namespace App\Domain\User\Entity;

use App\Domain\User\ValueObject\{UserId, Email, UserName};
use App\Domain\User\Event\UserRegisteredEvent;

final readonly class User
{
    public function __construct(
        public UserId $id,
        public Email $email,
        public UserName $name,
        public bool $active = true
    ) {}

    public static function register(Email $email, UserName $name): self
    {
        $user = new self(
            id: UserId::generate(),
            email: $email,
            name: $name
        );

        // Domain event - no framework dependency!
        DomainEvents::raise(new UserRegisteredEvent($user));

        return $user;
    }

    public function deactivate(): self
    {
        return new self(
            id: $this->id,
            email: $this->email,
            name: $this->name,
            active: false
        );
    }
}

Repository Interface (Domain layer):

// src/Domain/User/Repository/UserRepositoryInterface.php
namespace App\Domain\User\Repository;

use App\Domain\User\Entity\User;
use App\Domain\User\ValueObject\{UserId, Email};

interface UserRepositoryInterface
{
    public function find(UserId $id): ?User;
    public function findByEmail(Email $email): ?User;
    public function save(User $user): void;
    public function delete(User $user): void;
}

Repository Implementation (Infrastructure layer):

// src/Infrastructure/Persistence/User/DatabaseUserRepository.php
namespace App\Infrastructure\Persistence\User;

use App\Domain\User\Repository\UserRepositoryInterface;
use App\Domain\User\Entity\User;
use App\Framework\Database\EntityManager;

final readonly class DatabaseUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private EntityManager $entityManager
    ) {}

    public function find(UserId $id): ?User
    {
        return $this->entityManager->find(User::class, $id->value);
    }

    public function save(User $user): void
    {
        $this->entityManager->save($user);
    }
}

ServiceProvider (Infrastructure layer - wires dependencies):

// src/Infrastructure/ServiceProviders/UserServiceProvider.php
namespace App\Infrastructure\ServiceProviders;

use App\Framework\DI\Container;
use App\Domain\User\Repository\UserRepositoryInterface;
use App\Infrastructure\Persistence\User\DatabaseUserRepository;

final readonly class UserServiceProvider
{
    public function register(Container $container): void
    {
        // Wire Domain interface → Infrastructure implementation
        $container->singleton(
            UserRepositoryInterface::class,
            fn() => new DatabaseUserRepository(
                $container->get(EntityManager::class)
            )
        );
    }
}

Example 2: Application Layer (CQRS Pattern)

Structure:

src/Application/User/
├── Controller/
│   └── UserController.php
├── Command/                           # Write operations
│   ├── CreateUserCommand.php
│   └── CreateUserHandler.php
└── Query/                             # Read operations
    ├── GetUserQuery.php
    └── GetUserHandler.php

Controller (Thin - delegates to Command/Query handlers):

// src/Application/User/Controller/UserController.php
namespace App\Application\User\Controller;

use App\Framework\Attributes\Route;
use App\Framework\Http\{Method, JsonResult, Status};
use App\Application\User\Command\{CreateUserCommand, CreateUserHandler};
use App\Application\User\Query\{GetUserQuery, GetUserHandler};

final readonly class UserController
{
    public function __construct(
        private CreateUserHandler $createHandler,
        private GetUserHandler $getHandler
    ) {}

    #[Route(path: '/api/users', method: Method::POST)]
    public function create(CreateUserRequest $request): JsonResult
    {
        $command = new CreateUserCommand(
            $request->email,
            $request->name
        );

        $user = $this->createHandler->handle($command);

        return new JsonResult(
            ['id' => $user->id->value],
            status: Status::CREATED
        );
    }

    #[Route(path: '/api/users/{id}', method: Method::GET)]
    public function get(string $id): JsonResult
    {
        $query = new GetUserQuery(new UserId($id));
        $user = $this->getHandler->handle($query);

        return new JsonResult($user->toArray());
    }
}

Command Handler (Business logic):

// src/Application/User/Command/CreateUserHandler.php
namespace App\Application\User\Command;

use App\Domain\User\Entity\User;
use App\Domain\User\Repository\UserRepositoryInterface;

final readonly class CreateUserHandler
{
    public function __construct(
        private UserRepositoryInterface $repository
    ) {}

    public function handle(CreateUserCommand $command): User
    {
        // Domain logic
        $user = User::register($command->email, $command->name);

        // Persistence via Domain interface
        $this->repository->save($user);

        return $user;
    }
}

Alternative Approaches Considered

Alternative 1: Microservices Architecture

Why Rejected:

  • Premature optimization for current scale
  • Operational complexity (deployment, monitoring, debugging)
  • Network latency overhead
  • Distributed transaction challenges
  • Team not structured for microservices (no dedicated platform team)

Alternative 2: Event Sourcing + CQRS (Full)

Why Rejected:

  • Significant complexity increase
  • Learning curve for team
  • Infrastructure requirements (event store)
  • Eventual consistency challenges
  • Overkill for current business requirements

Alternative 3: Module-per-Repository (Monorepo)

Why Rejected:

  • Still duplicates Framework code per repository
  • Tooling complexity (cross-repo dependencies)
  • Versioning challenges
  • Overhead not justified for current team size

  • ADR-001: Use Custom PHP Framework (already decided)
  • ADR-002: Adopt Clean Architecture principles (already decided)
  • ADR-003: Use Composition over Inheritance (already decided)
  • ADR-004: Value Objects over Primitives (already decided)
  • ADR-005: Attribute-based Discovery (already decided)

This Decision: ADR-006 - Hybrid Layer-Module Architecture


References

Internal Documents

  • docs/SRC-ARCHITECTURE-ANALYSIS.md - Current architecture analysis
  • docs/DOCUMENTATION-ANALYSIS.md - Documentation structure
  • docs/claude/architecture.md - Framework architecture guide
  • docs/claude/guidelines.md - Development guidelines

Clean Architecture

  • "Clean Architecture" by Robert C. Martin
  • Dependency Rule: Dependencies point inward (Domain → Application → Framework)
  • Testability: Inner layers testable without outer layers

Domain-Driven Design

  • "Domain-Driven Design" by Eric Evans
  • Bounded Contexts: Clear boundaries between domains
  • Ubiquitous Language: Domain-specific terminology
  • Aggregates: Consistency boundaries

Framework Patterns

  • Martin Fowler's Enterprise Application Patterns
  • Repository Pattern: Data access abstraction
  • CQRS: Command-Query Responsibility Segregation
  • Event-Driven Architecture: Loose coupling via events

Timeline & Next Steps

Immediate Next Steps (Week 1)

Day 1-2: Phase 1 - Priority 1 Fixes

  • Move HTTP out of Domain (2 hours)
  • Move DI to Infrastructure (4 hours)
  • Fix Infrastructure nesting (1 hour)
  • Update namespaces (3 hours)
  • Total: 10 hours

Day 3: Testing & Validation

  • Run full test suite
  • PSR-4 compliance check
  • Architectural violation check
  • Total: 4 hours

Short-Term (Weeks 2-4): Phase 2 - Domain Modularization

  • Organize User domain (1 week)
  • Organize Order domain (1 week)
  • Organize Product, Asset, Cms domains (1 week)

Mid-Term (Weeks 5-8): Phase 3 - Application Modularization

  • Restructure User application layer (1 week)
  • Restructure Order application layer (1 week)
  • Restructure Admin feature module (2 weeks)

Long-Term (Weeks 9-12): Phase 4 - Infrastructure Consolidation

  • Create ServiceProviders (1 week)
  • Organize Persistence layer (1 week)
  • Migrate external service integrations (1 week)
  • Final testing & documentation (1 week)

Total Timeline: 12 weeks (3 months) - conservative estimate


Approval & Sign-Off

Decision Maker: Development Team Lead Date: 2025-01-28 Status: Recommended for Approval

Review Required By:

  • Technical Architect
  • Development Team
  • Product Owner
  • CTO

Implementation Start: After approval Estimated Completion: 3 months from start


Appendix A: Framework Layer Breakdown

Why Framework Must Be Shared (3,883 files - 85.3%):

src/Framework/
├── Core/                 (245 files) - Application bootstrap, routing, DI
├── Database/             (387 files) - EntityManager, Schema Builder, Migrations
├── Http/                 (156 files) - Request/Response, Middleware, Routing
├── Queue/                (89 files) - Job processing, Workers, Queue drivers
├── Cache/                (67 files) - Cache abstraction, Drivers (Redis, File)
├── ExceptionHandling/    (124 files) - Error handling, Exception types
├── Discovery/            (78 files) - Attribute discovery, Convention scanning
├── View/                 (201 files) - Template engine, Components
├── Events/               (95 files) - Event dispatcher, Domain events
├── Console/              (167 files) - CLI commands, TUI components
├── Validation/           (54 files) - Input validation, Rules
├── Security/             (89 files) - Authentication, Authorization, WAF
├── Logging/              (48 files) - Logger, Handlers
├── MCP/                  (123 files) - Model Context Protocol (AI integration)
└── [28 more components]  (1,960 files)

Total: 3,883 files

Duplication Impact: If pure module approach chosen, each module would duplicate most of these 3,883 files, resulting in:

  • 5 modules × 3,883 files = 19,415 files (vs current 3,883)
  • Maintenance nightmare: Update framework → update 5 copies
  • Consistency issues: Framework versions drift across modules
  • Lost investment: Years of framework development duplicated

Conclusion: Framework MUST be shared infrastructure, not duplicated per module.


Appendix B: PSR-4 Mapping

After Migration:

{
    "autoload": {
        "psr-4": {
            "App\\Domain\\": "src/Domain/",
            "App\\Application\\": "src/Application/",
            "App\\Framework\\": "src/Framework/",
            "App\\Infrastructure\\": "src/Infrastructure/"
        }
    }
}

Example Mappings:

Namespace                                          → File
App\Domain\User\Entity\User                       → src/Domain/User/Entity/User.php
App\Domain\User\Repository\UserRepositoryInterface → src/Domain/User/Repository/UserRepositoryInterface.php
App\Application\User\Command\CreateUserCommand     → src/Application/User/Command/CreateUserCommand.php
App\Infrastructure\Persistence\User\DatabaseUserRepository → src/Infrastructure/Persistence/User/DatabaseUserRepository.php
App\Framework\Database\EntityManager               → src/Framework/Database/EntityManager.php

Validation:

# Verify PSR-4 compliance
composer dump-autoload --optimize
composer validate --strict

# Check for PSR-4 violations
./vendor/bin/phpstan analyze --level=8

End of Document

Version: 1.0 Last Updated: 2025-01-28 Authors: Architecture Team Reviewers: Development Team