Post

Why "Premature" Architecture is Now Essential: Clean Architecture as AI Agent Superpower

Why "Premature" Architecture is Now Essential: Clean Architecture as AI Agent Superpower

I recently completed a significant refactoring of my frontend application to implement Clean Architecture during the MVP phase. This decision would have been considered premature overhead in the pre-AI development era. My investigation into the execution mechanics of AI coding agents suggests that this assumption is now inverted: robust architecture reduces total development time when the primary code generator is an AI agent rather than a human.

tldr; Clean Architecture’s explicit type contracts, dependency injection interfaces, and layer separation function as execution constraints for AI coding agents, reducing hallucinations and enabling parallel feature development without regression. The “overhead” of architectural setup is negated by the agent’s ability to generate compliant code rapidly when provided with sufficient structural context.

The Pre-AI Development Calculus

The conventional wisdom for early-stage startups has been to minimize architectural investment. The reasoning follows a straightforward cost-benefit analysis:

  1. Requirements are volatile; elaborate abstractions become liabilities when product direction changes.
  2. Engineering time is the primary constraint; time spent on architecture is time not spent on features.
  3. “Technical debt” can be addressed post-product-market-fit with dedicated refactoring sprints.

This calculus assumes that the human developer is the execution bottleneck. The developer must context-switch between writing business logic, handling API integration, managing UI state, and ensuring type safety. Under these conditions, adding architectural layers introduces cognitive overhead that slows iteration velocity.

The Post-AI Development Calculus

AI coding agents fundamentally alter this equation. The agent’s execution model differs from human cognition in several critical ways:

  1. Context Window as Working Memory: The agent operates within a fixed context window. Structured, well-documented code artifacts fit more efficiently into this window than monolithic components with implicit dependencies.

  2. Pattern Matching Over Reasoning: Agents generate code by pattern-matching against their training data and the provided context. Explicit interfaces and type definitions constrain the solution space, reducing the probability of generating incompatible or incorrect code.

  3. Parallel Execution: Unlike human developers, agents can be invoked in parallel across isolated codebases. Clean Architecture’s layer separation enables concurrent work on Domain, Infrastructure, and UI without merge conflicts or regression.

  4. Reduced Hallucination via Constraints: When an agent must implement a function that accepts IOnboardingService as a parameter, the interface definition constrains the available methods. The agent cannot hallucinate a method that does not exist on the interface.

The Refactoring: From Monolithic Hooks to Layered Architecture

My application previously used a pattern common in React applications: large custom hooks that combined state management, API calls, and business logic. The useOnboardingStep hook, for example, contained:

  • Local state initialization from multiple sources (server, localStorage)
  • Draft/final data merging logic
  • API mutation orchestration
  • Navigation control
  • Error handling

This pattern is convenient for rapid prototyping but creates several problems for AI-assisted development:

  1. The agent must understand the entire hook to modify any part of it.
  2. Changes to API structure require modifications across multiple concerns.
  3. Testing requires mocking the entire React environment.

The Architectural Document as Agent Context

I created an explicit architectural requirements document that serves as the primary context injection for the AI agent. This document specifies the layer structure, dependency rules, and implementation patterns.

The document begins with high-level principles:

1
2
3
4
5
6
7
8
9
10
## I. High-Level Architectural Principles

1. **Isolation from UI Framework:** The core logic (Business and Domain) 
   must be independent of React.
2. **Isolation from Server API:** The UI layer must be completely unaware 
   of the data access mechanism.
3. **Testability via Dependency Injection (DI):** Logic-heavy functions 
   must be refactored to use DI.
4. **Single Responsibility:** Code artifacts must have distinct, clearly 
   separated concerns.

This document functions as a prompt template. When I instruct the agent to “implement the Vision domain according to the architectural requirements,” the agent has explicit constraints that guide code generation.

Analysis: Layer Implementation

Domain Layer (Pure Business Logic)

The Domain layer contains entities and pure functions that operate on those entities. These artifacts have zero dependencies on React, API clients, or external libraries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// app/frontend/domain/commitment/entities.ts

export type TimeUnit = 'days' | 'weeks' | 'months'

export interface CommitmentEntity {
  deadline: number
  timeUnit: TimeUnit
  hoursPerDay: number
  includeSaturday: boolean
  includeSunday: boolean
  whyDeadline?: string
  completionDate?: Date
  totalHours?: number
  totalSessions?: number
  completedAt?: Date
}

export interface CommitmentMetrics {
  actualDays: number
  workingDays: number
  completionDate: Date
  formattedCompletionDate: string
  formattedWeeksAndDays: string
  totalHours: number
  totalSessions: number
}

The validation functions are pure, accepting entity data and returning structured results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app/frontend/domain/commitment/validation.ts

export interface CommitmentValidationResult {
  valid: boolean
  errors: Record<string, string>
}

export function validateCommitment(
  data: Partial<CommitmentEntity>
): CommitmentValidationResult {
  const errors: Record<string, string> = {}

  if (!isNumber(deadline) || deadline < 1) {
    errors.deadline = 'Deadline must be a positive number.'
  } else {
    const maxForUnit = MAX_BY_UNIT[timeUnit]
    if (deadline > maxForUnit) {
      errors.deadline = `Deadline cannot exceed ${maxForUnit} ${timeUnit}.`
    }
  }

  // Additional validation rules...

  return {
    valid: Object.keys(errors).length === 0,
    errors,
  }
}

AI Agent Benefit: When I instruct the agent to “add a validation rule for minimum hours per day,” the agent can modify validation.ts in isolation. The function’s pure signature (Partial<CommitmentEntity> → CommitmentValidationResult) constrains the implementation. The agent cannot introduce side effects or external dependencies without violating the type contract.

Infrastructure Layer (API Abstraction)

The Infrastructure layer handles all external communication. It defines DTOs that mirror API response structures and transformer functions that convert between DTOs and Domain entities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/frontend/infrastructure/onboarding/dtos.ts

export interface CommitmentStepDTO {
  completed_at?: string
  deadline_draft?: number
  timeUnit_draft?: 'days' | 'weeks' | 'months'
  hoursPerDay_draft?: number
  // ... additional fields
  deadline?: number
  timeUnit?: 'days' | 'weeks' | 'months'
  hoursPerDay?: number
  completionDate?: string
  totalHours?: number
  totalSessions?: number
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/frontend/infrastructure/onboarding/transformers.ts

export function toCommitmentEntity(dto: CommitmentStepDTO): CommitmentEntity {
  return {
    deadline: dto.deadline ?? dto.deadline_draft ?? 3,
    timeUnit: (dto.timeUnit ?? dto.timeUnit_draft ?? 'months') as TimeUnit,
    hoursPerDay: dto.hoursPerDay ?? dto.hoursPerDay_draft ?? 2,
    includeSaturday: dto.includeSaturday ?? dto.includeSaturday_draft ?? false,
    includeSunday: dto.includeSunday ?? dto.includeSunday_draft ?? false,
    whyDeadline: dto.whyDeadline ?? dto.whyDeadline_draft,
    completionDate: dto.completionDate ? new Date(dto.completionDate) : undefined,
    totalHours: dto.totalHours,
    totalSessions: dto.totalSessions,
    completedAt: dto.completed_at ? new Date(dto.completed_at) : undefined,
  }
}

The service class implements dependency injection for the API client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// app/frontend/infrastructure/onboarding/service.ts

export interface IOnboardingService {
  saveMissionDraft(step: number, sessionToken: string, mission: Partial<MissionEntity>): Promise<UpdateProgressResponseDTO>
  lockInMission(step: number, sessionToken: string, mission: Partial<MissionEntity>): Promise<UpdateProgressResponseDTO>
  saveCommitmentDraft(step: number, sessionToken: string, commitment: Partial<CommitmentEntity>): Promise<UpdateProgressResponseDTO>
  lockInCommitment(
    step: number,
    sessionToken: string,
    commitment: Partial<CommitmentEntity>,
    calculatedFields?: { completionDate?: Date; totalHours?: number; totalSessions?: number }
  ): Promise<UpdateProgressResponseDTO>
  saveVisionDraft(step: number, sessionToken: string, vision: Partial<VisionEntity>): Promise<UpdateProgressResponseDTO>
  lockInVision(step: number, sessionToken: string, vision: Partial<VisionEntity>): Promise<UpdateProgressResponseDTO>
}

export class OnboardingService implements IOnboardingService {
  constructor(private readonly apiClient: IApiClient) {}

  async lockInMission(
    step: number,
    sessionToken: string,
    mission: Partial<MissionEntity>
  ): Promise<UpdateProgressResponseDTO> {
    const progress = toMissionFinalDTO(mission)
    return updateOnboardingProgress(this.apiClient, { step, sessionToken, progress })
  }
  
  // Additional methods...
}

// Export singleton instance for application use
export const onboardingService = new OnboardingService(apiClient)

AI Agent Benefit: The IOnboardingService interface serves as a contract. When the agent generates code that depends on this service, it can only invoke methods defined in the interface. If I modify the API endpoint structure, I update the api.ts and transformers.ts files. The service interface remains stable, and no other layer requires modification.

Application Layer (Use Cases with Dependency Injection)

The Application layer contains Use Case functions that orchestrate business logic. These functions accept their dependencies as parameters, enabling unit testing without mocking frameworks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// app/frontend/application/onboarding/use-cases/lock-in-mission.ts

export interface LockInMissionDeps {
  onboardingService: Pick<IOnboardingService, 'lockInMission'>
  storage: Pick<Storage, 'setItem'>
}

export interface LockInMissionInput {
  step: number
  sessionToken: string
  mission: string
}

export interface LockInMissionResult {
  success: boolean
  entity: MissionEntity
}

export async function lockInMission(
  input: LockInMissionInput,
  deps: LockInMissionDeps
): Promise<LockInMissionResult> {
  const { step, sessionToken, mission } = input
  const { onboardingService, storage } = deps

  // Validate mission
  const trimmedMission = mission.trim()
  if (!trimmedMission) {
    throw new ValidationError('Mission cannot be empty')
  }

  // Create mission entity
  const entity: MissionEntity = {
    mission: trimmedMission,
    completedAt: new Date(),
  }

  // Call infrastructure service
  const response = await onboardingService.lockInMission(step, sessionToken, entity)

  if (response.success) {
    storage.setItem('outperformer_mission', trimmedMission)
    storage.setItem('onboarding_step', String(step + 1))
    storage.setItem('onboarding_started', new Date().toISOString())
  }

  return {
    success: response.success,
    entity,
  }
}

The React hooks serve as thin wrappers that inject production dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/frontend/application/onboarding/hooks/use-onboarding-mutations.ts

export function useLockInMission(
  options?: Omit<UseMutationOptions<LockInMissionResult, Error, LockInMissionInput>, 'mutationFn'>
): UseMutationResult<LockInMissionResult, Error, LockInMissionInput> {
  return useMutation({
    mutationFn: (input: LockInMissionInput) =>
      lockInMission(input, {
        onboardingService,
        storage: localStorage,
      }),
    ...options,
  })
}

AI Agent Benefit: The LockInMissionDeps interface explicitly declares the function’s external dependencies. When I instruct the agent to “add error logging to the lock-in flow,” the agent understands that it must either:

  1. Add a logger dependency to LockInMissionDeps, or
  2. Implement logging within the existing constraints.

The agent cannot introduce hidden dependencies that would complicate testing or violate layer boundaries.

UI Layer (Presentation Only)

The UI components now focus exclusively on rendering and event handling. State management and business logic are delegated to the Use Case hooks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// app/frontend/pages/Onboarding/MissionLock.tsx

export default function MissionLock({
  currentStep,
  sessionToken,
  progressData,
  nextStepUrl,
}: MissionLockProps) {
  const inputRef = useRef<HTMLInputElement | null>(null)
  const [mission, setMission] = useState<string>(() => {
    // Initialize from progress data or localStorage
    const step1 = progressData.step_1
    if (step1?.mission) return step1.mission
    if (step1?.mission_draft) return step1.mission_draft
    return ''
  })

  const [error, setError] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)

  // Use the specific mutation hooks from the Application layer
  const { mutate: lockIn, isPending: isLocking } = useLockInMission({
    onSuccess: () => {
      if (nextStepUrl) {
        router.visit(nextStepUrl)
      }
    },
    onError: (err) => {
      if (err instanceof ValidationError) {
        setErrorMessage(err.message)
        setError(true)
      }
    },
  })

  const { mutate: saveDraft } = useSaveMissionDraft()

  // Auto-save draft on mission change
  useEffect(() => {
    if (!mission.trim()) return
    const timeoutId = setTimeout(() => {
      saveDraft({ step: currentStep, sessionToken, mission })
    }, 500)
    return () => clearTimeout(timeoutId)
  }, [mission, currentStep, sessionToken, saveDraft])

  const handleLockIn = useCallback(() => {
    lockIn({ step: currentStep, sessionToken, mission: mission.trim() })
  }, [mission, currentStep, sessionToken, lockIn])

  // Render JSX...
}

AI Agent Benefit: The component’s responsibilities are clearly bounded. When I instruct the agent to “add a character counter to the mission input,” the agent modifies only the JSX and local state. The agent cannot accidentally introduce API calls or business logic into the component because the architecture enforces separation.

Quantitative Observations

During the refactoring process, I tracked the following metrics:

MetricPre-RefactoringPost-Refactoring
Files in frontend~2550+
Lines of code~2,400~2,600
Agent iterations per feature3-51-2
Regression incidents20

The increase in file count reflects the explicit layer separation. The modest increase in lines of code accounts for interface definitions, barrel exports, and type contracts. The reduction in agent iterations per feature is the primary value proposition: the agent generates correct code on the first attempt more frequently when provided with structural constraints.

Implications for Feature Development

The architectural investment enables several development patterns that would be problematic with monolithic components:

Parallel Feature Implementation

Consider adding a new onboarding step. The implementation requires:

  1. Domain entity and validation (domain/newstep/)
  2. DTOs and transformers (infrastructure/onboarding/)
  3. Use Case functions (application/onboarding/use-cases/)
  4. React hook (application/onboarding/hooks/)
  5. UI component (pages/Onboarding/)

Each of these can be implemented in isolation. I can instruct multiple agent sessions to work on different layers concurrently without merge conflicts.

Rapid Adaptation to Feedback

When user feedback indicates that the commitment deadline configuration is confusing, I can:

  1. Modify the domain validation rules without touching the UI.
  2. Update the UI component without modifying business logic.
  3. Change the API payload structure by updating only the DTOs and transformers.

The agent understands these boundaries because they are encoded in the type system.

Testing Strategy

Unit tests for pure domain functions execute in milliseconds:

1
2
3
4
5
6
7
8
9
10
11
describe('validateCommitment', () => {
  it('returns error when deadline exceeds maximum for unit', () => {
    const result = validateCommitment({
      deadline: 400,
      timeUnit: 'days',
      hoursPerDay: 2,
    })
    expect(result.valid).toBe(false)
    expect(result.errors.deadline).toContain('cannot exceed 365')
  })
})

Use Case functions can be tested with simple mock objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
describe('lockInMission', () => {
  it('stores mission in localStorage on success', async () => {
    const mockStorage = { setItem: vi.fn() }
    const mockService = { lockInMission: vi.fn().mockResolvedValue({ success: true }) }

    await lockInMission(
      { step: 1, sessionToken: 'token', mission: 'My Mission' },
      { onboardingService: mockService, storage: mockStorage }
    )

    expect(mockStorage.setItem).toHaveBeenCalledWith('outperformer_mission', 'My Mission')
  })
})

No React testing library required. No component mounting. Fast feedback cycles.

Conclusion

The economics of architectural investment have inverted with the introduction of AI coding agents. The explicit type contracts, interface definitions, and layer boundaries that constitute “overhead” in human-only development serve as execution constraints that improve agent output quality and reduce iteration cycles.

For early-stage startups using AI-assisted development, I recommend:

  1. Invest in architectural documentation early. The document functions as a reusable prompt template for all future agent interactions.

  2. Enforce layer boundaries via TypeScript. The type system prevents the agent from generating code that violates architectural constraints.

  3. Use dependency injection in business logic. The explicit dependency interfaces simplify both testing and agent comprehension.

  4. Separate DTOs from Domain entities. This isolation absorbs API changes without propagating modifications across the codebase.

The marginal cost of Clean Architecture in an AI-assisted workflow approaches zero. The marginal benefit—reduced hallucinations, parallel development, rapid adaptation—compounds with each feature added to the system.


Bonus (Reward for reading up to this point): The Refactoring process

Here’s the steps I followed for the refactoring and the actual Prompt I created for it. (It worked pretty well..!!)

I used two agents: One for execution (Claude Opus 4.5, via Cursor) and one for Auditing the result (Google Gemini 3 Pro, via cursor)

  1. I passed the Refactoring Prompt (shown below) to the executing agent and requested a Plan
  2. Reviewed the plan (did a couple adjustments)
  3. Executed the plan
  4. Reviewed the code base and identified gaps
  5. Spawned a new agent (Google Gemini 3 Pro) and tasked to Audit the refactoring against the refactoring specifications.
  6. Final review.

Refactoring Prompt (with specifications)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# Architectural Requirements Document for AI Agent Refactoring

**Project Goal:** To refactor an existing React application codebase to implement a clean, decoupled, and highly testable architecture based on principles derived from Clean Architecture and Domain-Driven Design (DDD).

**Target Architecture:** Layered structure (Domain, Infrastructure, Application, UI) focusing on separation of concerns, isolation from external dependencies (UI framework, server API), and simplification of testing,,.

## I. High-Level Architectural Principles

The AI agent must ensure the following core principles are met during the refactoring process:

1.  **Isolation from UI Framework:** The core logic (Business and Domain) must be independent of React, allowing the logic to be retained even if the UI framework changes,,.
2.  **Isolation from Server API:** The UI layer must be completely unaware of the data access mechanism (REST API, WebSocket, etc.) and the structure of raw server responses,,.
3.  **Testability via Dependency Injection (DI):** Logic-heavy functions (Infrastructure Services and Application Use Cases) must be refactored to use DI, enabling the use of simple, fast unit tests instead of complex, slow integration tests for covering logic branches,,,.
4.  **Single Responsibility:** Code artifacts must have distinct, clearly separated concerns (e.g., API clients handle connection, Services handle data transformation/logic, Components handle UI rendering),,.

---

## II. Layer-Specific Refactoring Requirements

The refactoring agent must create and organize the application code into the following distinct layers:

### A. Infrastructure Layer (Data Access and External Services)

This layer is responsible for dealing with external concerns, particularly the server API. The term `/api` folder must be replaced with a more generic name like `/infrastructure` to decouple the UI from knowing the specific underlying data connection (e.g., REST API),.

| Requirement                              | Details                                                                                                                                                                                                                                                                                                                  | Source |
| :--------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----- |
| **1. Shared API Client**                 | Create a central API client (e.g., using Axios or `fetch`) to house all common request configurations, such as `baseURL`, application-wide headers, and token passing logic,,. This ensures maintainability by requiring only a single line change if foundational parameters are updated,.                              | ,,     |
| **2. Extract Fetch Functions**           | Extract specific endpoint calls (`GET`, `POST`, exact paths) out of UI components into dedicated fetch functions (APIs) within the Infrastructure layer. These functions should return the actual application data structures, not raw API responses,,.                                                                  | ,      |
| **3. Handle DTOs and Transformations**   | Define **Data Transfer Objects (DTOs)** that strictly mirror the structure of the API responses (e.g., nested JSON:API structures),. Implement **Transformer Functions** within the Infrastructure layer that convert these DTOs into simplified **Domain Entities** before returning data to the application layer,.    | ,,     |
| **4. Implement Infrastructure Services** | For fetch functions that contain complex logic (e.g., switching between API versions, non-trivial data transformations),, refactor them into classes known as **Infrastructure Services**,. These services must implement **Dependency Injection (DI)** by accepting API client implementations via their constructor,,. | ,,     |
| **5. Expose Singletons**                 | Infrastructure Services should be exposed via a barrel file, typically as a **Singleton** instance, for consumption by the rest of the application,,. Simple API functions without logic may be exported directly.                                                                                                       | ,      |

### B. Domain Layer (Core Models and Logic)

The Domain Layer defines the core models (Entities) and logic that operates purely on those models. This logic must be free of UI and infrastructure dependencies,.

| Requirement                   | Details                                                                                                                                                                                                                                                                                                        | Source |
| :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----- |
| **1. Define Domain Entities** | Define simplified, flattened TypeScript interfaces for core models (e.g., `User`, `Image`) that are used across the application. These entities are decoupled from the server's nested DTO structure,.                                                                                                         | ,      |
| **2. Extract Domain Logic**   | Extract logic that operates purely on domain entities (like lookups, validation rules, or calculating derived state) from the UI components or Application Layer into dedicated, reusable functions within the Domain Layer,,. Examples include `getUserById(users, userId)` or `hasExceededShoutLimit(me)`,,. | ,      |
| **3. Enable Unit Testing**    | The extraction of domain logic into simple functions facilitates fast unit testing of complex logic (e.g., covering all branches related to block lists or shout limits) without relying on expensive integration tests,.                                                                                      | ,      |

### C. Application Layer (Business Logic and Use Cases)

This layer contains the application-specific business logic (Use Cases) that orchestrates calls between the Domain and Infrastructure layers. This layer manages state flow but is separated from UI rendering,.

| Requirement                                    | Details                                                                                                                                                                                                                                                                              | Source |
| :--------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----- |
| **1. Extract Business Logic to Use Cases**     | Extract complex component logic (e.g., form submit handlers containing validation, conditional checks, and orchestration of service calls) into standalone, framework-agnostic Use Case functions (e.g., `replyToShout`),,.                                                          | ,,     |
| **2. Use Dependency Injection in Use Cases**   | Use Case functions must accept their dependencies (Infrastructure Service functions or related services) as parameters to maintain isolation and simplify unit testing of all logic branches,.                                                                                       | ,      |
| **3. Integrate Server State Management**       | Implement custom React hooks (e.g., `useReplyToShout`, `useGetMe`) to leverage server state management libraries (like React Query) for data fetching and caching,. These hooks act as proxies, calling the underlying Infrastructure Services.                                      | ,      |
| **4. Optimize Use Case Hooks with Cache Data** | When incorporating React Query, Use Case hooks should be refactored to retrieve necessary data (e.g., `me` and `recipient` data) from the React Query cache using query hooks, rather than relying on direct service calls within the use case logic, to avoid duplicate requests,,. | ,      |

### D. UI Layer (Components)

The UI layer is responsible solely for rendering, event handling, and managing local state,.

| Requirement                   | Details                                                                                                                                                                                                                                                                                  | Source |
| :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----- |
| **1. Focus on Presentation**  | Components must primarily focus on rendering the UI and handling UI events (e.g., form submission, state updates like loading/error),.                                                                                                                                                   | ,      |
| **2. Minimize Logic**         | Components must minimize or eliminate all data validation, server orchestration, and complex data transformation logic, deferring those responsibilities to the Domain and Application layers,.                                                                                          | ,      |
| **3. Consume Use Case Hooks** | Components should interact with the Application Layer by calling Use Case hooks (which handle dependency injection and server state logic) or by calling query/mutation hooks directly, significantly slimming the submit handlers and removing manual loading/error state management,,. | ,,     |

---

## III. Testing Requirements

The architecture must actively promote the use of targeted unit testing for logic layers.

1.  **Unit Testing Infrastructure Services:** Services containing logic (like API version switching or complex data transformations) must be unit tested by mocking the API dependency via the interface provided during dependency injection,,.
2.  **Unit Testing Domain Logic:** Domain functions (e.g., validation rules or entity lookups) must be unit tested in isolation, covering all possible branches with fast tests to reduce the need for expensive integration tests,.
3.  **Unit Testing Application Logic (Use Cases):** Use Case functions must be unit tested by providing mock implementations of all external dependencies (services) to verify that the core business flow and conditional branches are handled correctly,.
4.  **Integration Test Reduction:** By isolating and unit testing business and domain logic, the number of required integration or end-to-end tests for scenario coverage should be significantly reduced, only focusing on testing features integrated into the larger system,,.
This post is licensed under CC BY 4.0 by the author.