The Decision to Evolve
Following the insights from Phase 4.5, where I discovered that specs should reflect reality, not intentions, Phase 4.6 emerged as the natural next step. The previous phases had taught me critical lessons about AI limitations and the need for physical boundaries over prompts.
After progressing through experiments with convention generation, project setup automation, and AI-assisted TDD workflows, it became clear that my monolithic repository structure was creating unnecessary friction. When AI has access to everything, it tends to modify files it shouldn't touch. The solution wasn't better prompts - it was better architecture.
The New Architecture: From One to Many
Foundation Layer: Parent POMs
The split began with establishing solid foundations through two distinct parent POM structures:
General Parent POM (parent-pom): The cornerstone of my framework, this repository defines all actual versions of commonly used dependencies. Following the same approach as Spring Boot's dependency management, updating versions across all projects becomes as simple as updating the parent POM version in child projects. I also leverage the Maven versions plugin to automatically update minor versions of dependencies during the build process.
Spring-Specific Parent POM (parent-pom-spring): This specialized parent extends the general POM with Spring-specific configurations, dependencies, and best practices. It also defines the dependencies for the common libraries described below in this post. This separation allows non-Spring projects to remain lightweight while Spring projects get everything they need out-of-the-box, all with centralized version management.
Core Libraries: The Commons Repositories
The libs repository became my central hub for shared, domain-agnostic functionality. This collection of common libraries eliminates code duplication and provides consistent patterns across all projects:
common-utils: Framework and domain-agnostic utility functions including Kotlin extension functions and other utilities like UUID generation. These are the building blocks that every project needs but shouldn't have to implement from scratch.
common-testing: Testing infrastructure including the Rand object for generating random test values (developed in previous phases), Spring testing configurations, and Docker container setup helpers. This library standardizes my testing approach across all services.
common-dto: Universal data transfer objects applicable to any project and domain. Contains foundational DTOs like ModelV1, PageV1, and PageQueryV1 that establish consistent API patterns across the entire ecosystem.
common-service: Organized into modules by service layer (api, client, domain), this library provides domain-agnostic infrastructure including common exception objects and handlers, base models, mappers, and validators that form the backbone of my hexagonal architecture implementation.
Practical Implementation: The Users API Demo
To validate my new architecture and provide concrete examples, I created a comprehensive users API project that showcases how all these pieces work together in practice. It demonstrates my framework in action through a complete user management system built on the new modular architecture.
Users Service: TDD-Built with Hexagonal Architecture
The users service was built using Test-Driven Development with behavior tests. While this step was done manually, it serves as an excellent example for future AI-assisted development. The service implements a modular Maven project with three main modules following hexagonal architecture principles:
Service Module: Contains submodules organized by hexagonal architecture layers (domain, application, infrastructure), ensuring clean separation between business logic and external concerns. This structure makes the codebase more testable and maintainable while providing clear boundaries for AI to understand.
Client Module: Provides a clean interface for communication with the users service. This module contains submodules with the API interface, different implementations (currently internal-only, but designed to support REST, WebSockets, and other protocols), and Spring configuration for seamless integration into other services.
DTO Module: Houses common data transfer objects shared between the service and client modules, ensuring consistent data contracts and preventing duplication of model definitions.
API Layer: Integration With Public World
The API component serves as the gateway and includes the entire users service as a dependency - not just the client, but the whole service. This creates a monolithic deployment while maintaining modular development, giving us the best of both worlds.
Current Module Structure:
- Specialized API Modules: Organized by purpose and audience (api-admin, api-public, api-mobile), each module can be tailored for specific use cases with appropriate security, rate limiting, and feature sets
- App Module: Runs the entire application as a single deployable unit
Testing Philosophy: This layer tests only its specific responsibilities - ensuring controller endpoints are properly designed and input validation works correctly. There are no mocks; tests call the real users service with a real database. The tests remain remarkably simple (see the actual tests). For testing, I use Ktor client instead of traditional MockMvc because MockMvc is complex and unpredictable.
Future Scaling Strategy: The modular design allows individual API modules to be extracted into standalone servers later, enabling horizontal scaling and team-specific deployment cycles as the system grows.
Why This Split Matters
Modularity and Reusability
Each repository now has a clear, single responsibility. The parent POMs can be reused across any number of projects, the commons libraries provide consistent functionality, and the users API shows how everything integrates.
Independent Development Cycles
Different components can now be developed independently. Changes to the commons libraries don't require touching the parent POMs, and new services can be created without affecting existing ones.
Centralized Version Management
With parent POMs managing all dependency versions and the Maven versions plugin automatically handling minor updates, version conflicts become a thing of the past. A single parent POM update propagates consistently across the entire ecosystem.
Scalability
This structure naturally supports the addition of new services, libraries, and configurations as the framework grows. Each new component finds its appropriate place within the established architecture.
Looking Forward
With the repository structure in place, I'm going to create templates for both the service and API layers. Then I'll continue my experiments with AI to generate tests and implementations.
However, a quick test revealed an issue: having commons libraries creates overhead for AI. Claude (and other AI models) don't inherently know about these custom libraries, and asking AI to check JAR files or external documentation disrupts the workflow.
The solution is to build a knowledge base and compile it into a simple format that AI can understand - making the commons functionality discoverable without breaking the development flow.
This repository split demonstrates a core VibeTDD principle: when working with AI, make good behaviors easy and bad behaviors impossible through architecture, not instructions.
Top comments (0)