- 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
33 KiB
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 analysisDOCUMENTATION-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:
-
HTTP Layer in Domain (
src/Domain/Meta/Http/)- Controllers with
#[Route]attributes in Domain layer - 406 lines of HTTP-specific code misplaced
- Controllers with
-
DI Container Setup in Domain (5 domains affected)
- Framework-specific dependency injection in Domain
- Violates Dependency Rule (Domain depends on Framework)
-
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
- Framework Size: 85.3% of codebase is framework infrastructure
- Code Duplication Risk: Pure modules would replicate framework per module
- Clean Architecture Benefits: Dependency inversion, testability, maintainability
- Domain-Driven Design Goals: Bounded contexts, domain focus
- Refactoring Effort: Time and risk considerations
- Team Familiarity: Existing knowledge of current structure
- 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:
-
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
-
Clean Architecture Preserved: Maintains dependency rule while adding modularity
- Domain depends on nothing
- Application depends on Domain
- Framework provides infrastructure
- Infrastructure implements interfaces
-
Domain-Driven Design Benefits: Modules represent bounded contexts
Domain/User/- user bounded contextDomain/Order/- order bounded contextDomain/Product/- product bounded context- Clear boundaries, no cross-domain dependencies
-
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)
-
Risk Mitigation: Incremental changes, testable at each phase
Implementation Strategy
Phase 1: Priority 1 Fixes (2-3 days) - CRITICAL
Objective: Fix architectural violations
Tasks:
-
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/ -
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 -
Fix Infrastructure Nesting
mv src/Application/Website/Infrastructure/GeoIp/ \ src/Infrastructure/GeoIp/ -
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
-
Clean Architecture Compliance
- All layers respect Dependency Rule
- Domain layer pure (no framework dependencies)
- Testability improved (mock infrastructure easily)
-
Domain-Driven Design Benefits
- Clear bounded contexts (User, Order, Product)
- Domain logic focused and cohesive
- Business rules visible and maintainable
-
Framework Preservation
- No code duplication (Framework shared)
- Single framework to maintain
- Consistency across all modules
- Framework investment preserved (3,883 files)
-
Maintainability
- Clear structure reduces cognitive load
- Easy to locate code (Domain/User/Entity/User.php)
- Self-documenting architecture
- Onboarding new developers easier
-
Realistic Timeline
- 2-3 months total effort
- Incremental changes (low risk)
- Testable at each phase
Negative Consequences (Mitigated)
-
Framework is Shared Dependency
- Mitigation: Framework is stable, well-tested infrastructure
- Trade-off: Accepting this trade-off for massive code reuse benefit
-
Module Deployment Coupling
- Mitigation: Not pursuing microservices architecture (monolith is appropriate)
- Trade-off: Deployment simplicity vs independent module deployment
-
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
Related Decisions
- 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 analysisdocs/DOCUMENTATION-ANALYSIS.md- Documentation structuredocs/claude/architecture.md- Framework architecture guidedocs/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