🏗 Architect view
Coupling, patterns, Zones of Pain, refactoring priorities · Software Architect, Principal Engineer
⚡ Agent trace · 13 steps · $0.0261 · click to expand
search_claims(query='architecture layer pattern', category='structural') →
search_claims(query='module package component', category='structural') →
search_claims(query='class interface abstract', max_results=20) →
search_claims(query='import from', max_results=20) →
search_claims(query='dependency injection', max_results=20) →
search_claims(query='package io.spring', max_results=30) →
search_claims(query='core domain entity', max_results=20) →
search_claims(query='io.spring.core', max_results=30) →
search_claims(query='io.spring.application', max_results=30) →
search_claims(query='io.spring.infrastructure', max_results=30) →
search_claims(query='file', category='structural') →
get_file_claims(file_path='src/main/java/io/spring/core/article/ArticleRepository.java') →
get_file_claims(file_path='src/main/java/io/spring/application/TagsQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/application/TagsQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/application/ProfileQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/core/article/ArticleRepository.java') →
get_dependencies(file_path='src/main/java/io/spring/application/ProfileQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java') →
get_dependencies(file_path='src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java') →
get_file_claims(file_path='src/main/java/io/spring/core/user/User.java') →
get_file_claims(file_path='src/main/java/io/spring/core/article/Article.java') →
get_file_claims(file_path='src/main/java/io/spring/infrastructure/repository/MyBatisArticleRepository.java') →
get_dependencies(file_path='src/main/java/io/spring/application/ArticleQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/application/CommentQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/application/UserQueryService.java') →
get_dependencies(file_path='src/main/java/io/spring/api/ArticleApi.java') →
get_dependencies(file_path='src/main/java/io/spring/graphql/ArticleDatafetcher.java') →
get_dependencies(file_path='src/main/java/io/spring/graphql/ArticleMutation.java') →
get_dependencies(file_path='src/main/java/io/spring/core/service/AuthorizationService.java') →
get_dependencies(file_path='src/main/java/io/spring/core/service/JwtService.java') →
get_dependencies(file_path='src/main/java/io/spring/core/user/UserRepository.java') →
get_dependencies(file_path='src/main/java/io/spring/application/article/ArticleCommandService.java') →
get_dependencies(file_path='src/main/java/io/spring/application/user/UserService.java') →
get_dependencies(file_path='src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleReadService.java')
Architectural Analysis
Architecture Overview
This is a RealWorld (Conduit) blog platform built with Spring Boot, following a layered architecture with four primary layers: core (domain), application (use cases), infrastructure (persistence/security), and api/graphql (presentation). The system exposes both a REST API (io.spring.api) and a GraphQL API (io.spring.graphql via Netflix DGS), backed by MyBatis for persistence and JWT for authentication. The domain layer defines repository interfaces and entities, the application layer orchestrates use cases, and the infrastructure layer provides concrete implementations. However, the architecture exhibits significant violations of the Dependency Inversion Principle, with the application layer directly depending on infrastructure read services rather than depending on abstractions defined in the domain or application layer.
Zones of Pain
1. io.spring.application — The Most Violated Layer (HIGH)
The application layer is supposed to be a pure use-case orchestration layer that depends only on domain abstractions. Instead, every query service in the application layer directly imports infrastructure read services:
| Application Service | Infrastructure Dependency | File |
|---|---|---|
ArticleQueryService |
ArticleReadService, ArticleFavoritesReadService, UserRelationshipQueryService |
src/main/java/io/spring/application/ArticleQueryService.java:3-7 |
CommentQueryService |
CommentReadService, UserRelationshipQueryService |
src/main/java/io/spring/application/CommentQueryService.java:3-4 |
ProfileQueryService |
UserReadService, UserRelationshipQueryService |
src/main/java/io/spring/application/ProfileQueryService.java:3-5 |
TagsQueryService |
TagReadService |
src/main/java/io/spring/application/TagsQueryService.java:3-3 |
UserQueryService |
UserReadService |
src/main/java/io/spring/application/UserQueryService.java:3-3 |
This means every query path in the system bypasses the domain layer entirely. The domain layer (io.spring.core) defines repository interfaces for writes (ArticleRepository, UserRepository, etc.) but no interfaces for reads. The read side is entirely coupled to MyBatis mapper interfaces.
2. io.spring.core.service — Domain Services with No Abstraction (MEDIUM)
The core.service package contains JwtService (an interface) and AuthorizationService (a concrete class). While JwtService is properly abstracted with an implementation in infrastructure.service, AuthorizationService is a concrete class in the domain layer that directly checks entity ownership:
src/main/java/io/spring/core/service/AuthorizationService.java— concrete class in domain, imported by bothapiandgraphqllayers
This is a minor concern but indicates the domain layer is mixing pure domain logic with what could be application-layer authorization concerns.
3. io.spring.graphql — Cross-Layer Exception Coupling (MEDIUM)
The GraphQL layer imports exception classes from the API layer:
src/main/java/io/spring/graphql/ArticleDatafetcher.java:3— importsio.spring.api.exception.ResourceNotFoundExceptionsrc/main/java/io/spring/graphql/ArticleMutation.java:3-4— importsio.spring.api.exception.NoAuthorizationExceptionandResourceNotFoundExceptionsrc/main/java/io/spring/graphql/CommentMutation.java— same pattern
This creates a circular-like dependency where two presentation layers (REST and GraphQL) should be independent peers but instead share exception types. These exceptions should live in a shared module or the application layer.
Coupling Analysis
| Module | In-Degree | Out-Degree | Concern Level | Reasoning |
|---|---|---|---|---|
io.spring.core.article.ArticleRepository |
16 | 0 | HIGH | Every command path depends on this interface; changing it cascades across 16 consumers |
io.spring.core.user.UserRepository |
18 | 0 | HIGH | The most depended-upon interface; 18 consumers across all layers |
io.spring.core.user.User |
~20 | 0 | HIGH | Domain entity used by every layer; any field change is high-impact |
io.spring.application.ArticleQueryService |
10 | 3 | HIGH | Bridges API/GraphQL to infrastructure; violates DIP |
io.spring.application.ProfileQueryService |
5 | 2 | HIGH | Directly depends on infrastructure read services |
io.spring.infrastructure.mybatis.readservice.* |
1-3 each | 0-1 | MEDIUM | No abstractions; MyBatis mappers are the de-facto read interface |
io.spring.api.exception.* |
3 (from graphql) | 0 | MEDIUM | Shared between presentation layers inappropriately |
io.spring.core.service.JwtService |
7 | 1 | LOW | Properly abstracted with interface/impl separation |
io.spring.core.service.AuthorizationService |
4 | 0 | LOW | Concrete class in domain; minor concern |
Design Pattern Inventory
Repository Pattern — ⚠️ INCONSISTENT
- Write side: Properly implemented —
ArticleRepository,UserRepository,CommentRepository,ArticleFavoriteRepositoryare interfaces inio.spring.core.*with MyBatis implementations inio.spring.infrastructure.repository - Read side: Missing entirely — No repository interfaces for queries. The application layer calls
*ReadServiceinterfaces directly fromio.spring.infrastructure.mybatis.readservice. This is a half-implemented Repository pattern.
Service Layer Pattern — ⚠️ INCONSISTENT
- Command services (
ArticleCommandService,UserService) properly depend only on domain abstractions - Query services (
ArticleQueryService,ProfileQueryService, etc.) violate the pattern by depending on infrastructure
Dependency Injection — ✅ CONSISTENT
- Constructor injection via Lombok
@AllArgsConstructoris used uniformly across all layers - Spring
@Component/@Service/@Repositoryannotations are consistently applied
Data Mapper Pattern (MyBatis) — ✅ CONSISTENT
- MyBatis
@Mapperinterfaces inio.spring.infrastructure.mybatis.mapperhandle persistence - Read services in
io.spring.infrastructure.mybatis.readservicehandle queries
GraphQL DGS Pattern — ✅ CONSISTENT
@DgsComponent+@DgsDataannotations used uniformly- Data fetchers are cleanly separated by domain concept
Refactoring Priorities
P1: Introduce Read-Side Repository Interfaces in Domain Layer (HIGH — 3-5 sprints)
Problem: The application layer depends directly on io.spring.infrastructure.mybatis.readservice.* interfaces. This means:
- You cannot swap MyBatis for another persistence technology without changing application logic
- Testing query services requires a full Spring context with MyBatis mocks
- The domain layer has no contract for read operations
Solution: Define read repository interfaces in io.spring.core.* (e.g., ArticleReadRepository, UserReadRepository) that the application layer depends on. Move the MyBatis implementations to io.spring.infrastructure.repository or a dedicated io.spring.infrastructure.readrepository package.
Files affected: ArticleQueryService.java, ProfileQueryService.java, CommentQueryService.java, TagsQueryService.java, UserQueryService.java, plus 5 new interfaces and 5 new implementation classes.
Expected benefit: Clean DIP compliance, testable application layer, swappable persistence.
P2: Extract Shared Exception Types to Application Layer (MEDIUM — 1 sprint)
Problem: io.spring.graphql imports io.spring.api.exception.*, creating an inappropriate dependency between two presentation-layer peers.
Solution: Move NoAuthorizationException and ResourceNotFoundException to io.spring.application.exception (or a new io.spring.common.exception package). Both api and graphql layers should depend on the application layer, not on each other.
Files affected: 2 exception classes, 3 GraphQL files, 2 API files.
Expected benefit: Clean layer isolation; GraphQL and REST become truly independent.
P3: Abstract AuthorizationService Behind an Interface (LOW — 1 sprint)
Problem: io.spring.core.service.AuthorizationService is a concrete class in the domain layer. While it only uses domain entities, making it an interface would allow different authorization strategies (e.g., role-based vs. ownership-based) without modifying domain code.
Solution: Extract AuthorizationService interface in io.spring.core.service, rename current implementation to OwnershipAuthorizationService and move to io.spring.application.service.
Files affected: 1 interface creation, 1 class rename/move, 4 consumer files.
Expected benefit: Domain layer becomes purely declarative; authorization strategy becomes swappable.
P4: Reduce UserRepository Fan-Out (LOW — 2-3 sprints)
Problem: UserRepository has 18 consumers across all layers. This is the single highest-coupling point in the system. Any change to this interface (e.g., adding pagination, changing return types) requires changes in 18 files.
Solution: Consider splitting UserRepository into role-specific interfaces (e.g., UserAuthenticationRepository, UserProfileRepository, UserAdminRepository) or using the Interface Segregation Principle with smaller, focused interfaces.
Expected benefit: Reduced blast radius for changes; clearer contracts per use case.