🏗 Architect view

Coupling, patterns, Zones of Pain, refactoring priorities  ·  Software Architect, Principal Engineer

Generated 4 days ago
⚡ 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 both api and graphql layers

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:

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, ArticleFavoriteRepository are interfaces in io.spring.core.* with MyBatis implementations in io.spring.infrastructure.repository
  • Read side: Missing entirely — No repository interfaces for queries. The application layer calls *ReadService interfaces directly from io.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 @AllArgsConstructor is used uniformly across all layers
  • Spring @Component/@Service/@Repository annotations are consistently applied

Data Mapper Pattern (MyBatis) — ✅ CONSISTENT

  • MyBatis @Mapper interfaces in io.spring.infrastructure.mybatis.mapper handle persistence
  • Read services in io.spring.infrastructure.mybatis.readservice handle queries

GraphQL DGS Pattern — ✅ CONSISTENT

  • @DgsComponent + @DgsData annotations 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.