On this page
Clean Architecture / Hexagonal / Onion Pattern
Date: 2025-22-09
Status: accepted
Current Architecture Analysis
The Vega web application is built with Remix (React Router v7) and follows a feature-based routing structure with the following characteristics:
Current Structure
services/remix/
├── app/
│ ├── components/ # Shared components
│ │ ├── ui/ # Design system components (shadcn/ui based)
│ │ ├── chat/ # Chat-related components
│ │ ├── forms/ # Form components
│ │ └── shared/ # Shared business components
│ ├── routes/ # Route-based organization
│ │ ├── alerts+/ # Alerts feature
│ │ ├── dashboards+/ # Dashboards feature
│ │ ├── detections+/ # Detections feature
│ │ ├── assessment+/ # Data assessment feature
│ │ ├── intel+/ # Intelligence feature
│ │ ├── notebooks+/ # Notebooks feature
│ │ ├── query+/ # Query feature
│ │ ├── settings+/ # Settings feature
│ │ └── _auth+/ # Authentication
│ ├── queries/ # GraphQL queries
│ ├── utils/ # Utility functions
│ └── transformations/ # Data transformations
Key Technologies
- Framework: Remix (React Router v7) with Vite
- UI Library: shadcn/ui components + Radix UI primitives
- State Management: Zustand + React Query
- Styling: Tailwind CSS
- Data Fetching: GraphQL with custom hooks
- Testing: Vitest + Playwright
Clean Architecture Migration
What is Clean Architecture?
Clean Architecture (also known as Hexagonal or Onion Architecture) is an architectural pattern that emphasizes:
- Independence: Business logic is independent of frameworks, UI, and external systems
- Testability: Business logic can be tested without external dependencies
- Flexibility: Easy to change external systems without affecting business logic
- Dependency Inversion: Dependencies point inward toward the core
Clean Architecture Layers
src/
├── domain/ # Core business logic (innermost layer)
│ ├── entities/ # Core business entities
│ ├── use-cases/ # Application business rules
│ ├── repositories/ # Repository interfaces
│ └── services/ # Domain services
├── application/ # Application layer
│ ├── use-cases/ # Use case implementations
│ ├── services/ # Application services
│ ├── dto/ # Data transfer objects
│ └── ports/ # Port interfaces
├── infrastructure/ # External concerns (outermost layer)
│ ├── repositories/ # Repository implementations
│ ├── services/ # External service implementations
│ ├── database/ # Database implementations
│ └── external/ # External API implementations
├── presentation/ # UI layer
│ ├── components/ # React components
│ ├── pages/ # Page components
│ ├── hooks/ # Custom hooks
│ └── adapters/ # UI adapters
└── shared/ # Shared utilities
├── types/ # Shared types
├── utils/ # Utility functions
└── constants/ # Constants
Migration Strategy
Phase 1: Extract Domain Layer
Step 1: Domain Entities
// src/domain/entities/User.ts
export class User {
constructor(
public readonly id: string,
public readonly email: string,
public readonly name: string,
public readonly role: string,
public readonly permissions: string[],
public readonly createdAt: Date,
public readonly updatedAt: Date,
public readonly avatar?: string
) {}
hasPermission(permission: string): boolean {
return this.permissions.includes(permission);
}
isAdmin(): boolean {
return this.role === 'admin';
}
canAccess(resource: string): boolean {
return this.permissions.includes(`access:${resource}`);
}
static create(data: {
id: string;
email: string;
name: string;
role: string;
permissions: string[];
avatar?: string;
}): User {
const now = new Date();
return new User(
data.id,
data.email,
data.name,
data.role,
data.permissions,
now,
now,
data.avatar
);
}
}
// src/domain/entities/Detection.ts
export class Detection {
constructor(
public readonly id: string,
public readonly name: string,
public readonly description: string,
public readonly query: string,
public readonly severity: DetectionSeverity,
public readonly status: DetectionStatus,
public readonly mitreTechniques: string[],
public readonly createdBy: string,
public readonly createdAt: Date,
public readonly updatedAt: Date
) {}
isActive(): boolean {
return this.status === DetectionStatus.ACTIVE;
}
isHighSeverity(): boolean {
return this.severity === DetectionSeverity.HIGH || this.severity === DetectionSeverity.CRITICAL;
}
canBeEditedBy(user: User): boolean {
return user.id === this.createdBy || user.hasPermission('detection:edit');
}
static create(data: {
name: string;
description: string;
query: string;
severity: DetectionSeverity;
createdBy: string;
mitreTechniques?: string[];
}): Detection {
const now = new Date();
return new Detection(
crypto.randomUUID(),
data.name,
data.description,
data.query,
data.severity,
DetectionStatus.DRAFT,
data.mitreTechniques || [],
data.createdBy,
now,
now
);
}
}
export enum DetectionSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum DetectionStatus {
DRAFT = 'draft',
ACTIVE = 'active',
INACTIVE = 'inactive'
}Step 2: Domain Services
// src/domain/services/DetectionService.ts
import { Detection, DetectionSeverity } from '../entities/Detection';
import { User } from '../entities/User';
export interface DetectionService {
validateDetection(detection: Detection): Promise<ValidationResult>;
calculateRiskScore(detection: Detection): number;
getRecommendedMitreTechniques(query: string): Promise<string[]>;
}
export class DetectionDomainService implements DetectionService {
async validateDetection(detection: Detection): Promise<ValidationResult> {
const errors: string[] = [];
if (!detection.name.trim()) {
errors.push('Detection name is required');
}
if (!detection.query.trim()) {
errors.push('Detection query is required');
}
if (detection.query.length < 10) {
errors.push('Detection query must be at least 10 characters');
}
return {
isValid: errors.length === 0,
errors
};
}
calculateRiskScore(detection: Detection): number {
let score = 0;
switch (detection.severity) {
case DetectionSeverity.LOW:
score += 1;
break;
case DetectionSeverity.MEDIUM:
score += 2;
break;
case DetectionSeverity.HIGH:
score += 3;
break;
case DetectionSeverity.CRITICAL:
score += 4;
break;
}
if (detection.mitreTechniques.length > 0) {
score += detection.mitreTechniques.length * 0.5;
}
return Math.min(score, 10);
}
async getRecommendedMitreTechniques(query: string): Promise<string[]> {
// This would typically call an external service or use ML
// For now, return mock data based on query keywords
const keywords = query.toLowerCase();
const techniques: string[] = [];
if (keywords.includes('powershell')) {
techniques.push('T1059.001');
}
if (keywords.includes('registry')) {
techniques.push('T1012');
}
if (keywords.includes('network')) {
techniques.push('T1043');
}
return techniques;
}
}
export interface ValidationResult {
isValid: boolean;
errors: string[];
}Step 3: Repository Interfaces
// src/domain/repositories/UserRepository.ts
import { User } from '../entities/User';
export interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<User>;
delete(id: string): Promise<void>;
findAll(): Promise<User[]>;
}
// src/domain/repositories/DetectionRepository.ts
import { Detection } from '../entities/Detection';
export interface DetectionRepository {
findById(id: string): Promise<Detection | null>;
findAll(): Promise<Detection[]>;
findByStatus(status: string): Promise<Detection[]>;
findBySeverity(severity: string): Promise<Detection[]>;
save(detection: Detection): Promise<Detection>;
update(id: string, detection: Partial<Detection>): Promise<Detection>;
delete(id: string): Promise<void>;
}Phase 2: Extract Application Layer
Step 1: Use Cases
// src/application/use-cases/CreateDetectionUseCase.ts
import { Detection, DetectionSeverity } from '../../domain/entities/Detection';
import { DetectionRepository } from '../../domain/repositories/DetectionRepository';
import { DetectionService } from '../../domain/services/DetectionService';
import { UserRepository } from '../../domain/repositories/UserRepository';
export interface CreateDetectionRequest {
name: string;
description: string;
query: string;
severity: DetectionSeverity;
createdBy: string;
}
export interface CreateDetectionResponse {
detection: Detection;
riskScore: number;
}
export class CreateDetectionUseCase {
constructor(
private detectionRepository: DetectionRepository,
private detectionService: DetectionService,
private userRepository: UserRepository
) {}
async execute(request: CreateDetectionRequest): Promise<CreateDetectionResponse> {
// Validate user exists
const user = await this.userRepository.findById(request.createdBy);
if (!user) {
throw new Error('User not found');
}
// Create detection entity
const detection = Detection.create({
name: request.name,
description: request.description,
query: request.query,
severity: request.severity,
createdBy: request.createdBy
});
// Validate detection
const validation = await this.detectionService.validateDetection(detection);
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
// Get recommended MITRE techniques
const mitreTechniques = await this.detectionService.getRecommendedMitreTechniques(request.query);
const detectionWithMitre = new Detection(
detection.id,
detection.name,
detection.description,
detection.query,
detection.severity,
detection.status,
mitreTechniques,
detection.createdBy,
detection.createdAt,
detection.updatedAt
);
// Save detection
const savedDetection = await this.detectionRepository.save(detectionWithMitre);
// Calculate risk score
const riskScore = this.detectionService.calculateRiskScore(savedDetection);
return {
detection: savedDetection,
riskScore
};
}
}
// src/application/use-cases/GetDetectionsUseCase.ts
import { Detection } from '../../domain/entities/Detection';
import { DetectionRepository } from '../../domain/repositories/DetectionRepository';
import { User } from '../../domain/entities/User';
import { UserRepository } from '../../domain/repositories/UserRepository';
export interface GetDetectionsRequest {
userId: string;
status?: string;
severity?: string;
}
export interface GetDetectionsResponse {
detections: Detection[];
totalCount: number;
}
export class GetDetectionsUseCase {
constructor(
private detectionRepository: DetectionRepository,
private userRepository: UserRepository
) {}
async execute(request: GetDetectionsRequest): Promise<GetDetectionsResponse> {
// Validate user exists
const user = await this.userRepository.findById(request.userId);
if (!user) {
throw new Error('User not found');
}
let detections: Detection[];
if (request.status) {
detections = await this.detectionRepository.findByStatus(request.status);
} else if (request.severity) {
detections = await this.detectionRepository.findBySeverity(request.severity);
} else {
detections = await this.detectionRepository.findAll();
}
// Filter detections based on user permissions
const accessibleDetections = detections.filter(detection =>
detection.canBeEditedBy(user) || user.hasPermission('detection:read')
);
return {
detections: accessibleDetections,
totalCount: accessibleDetections.length
};
}
}Step 2: Application Services
// src/application/services/DetectionApplicationService.ts
import { CreateDetectionUseCase, CreateDetectionRequest, CreateDetectionResponse } from '../use-cases/CreateDetectionUseCase';
import { GetDetectionsUseCase, GetDetectionsRequest, GetDetectionsResponse } from '../use-cases/GetDetectionsUseCase';
import { Detection } from '../../domain/entities/Detection';
import { DetectionRepository } from '../../domain/repositories/DetectionRepository';
import { DetectionService } from '../../domain/services/DetectionService';
import { UserRepository } from '../../domain/repositories/UserRepository';
export class DetectionApplicationService {
private createDetectionUseCase: CreateDetectionUseCase;
private getDetectionsUseCase: GetDetectionsUseCase;
constructor(
detectionRepository: DetectionRepository,
detectionService: DetectionService,
userRepository: UserRepository
) {
this.createDetectionUseCase = new CreateDetectionUseCase(
detectionRepository,
detectionService,
userRepository
);
this.getDetectionsUseCase = new GetDetectionsUseCase(
detectionRepository,
userRepository
);
}
async createDetection(request: CreateDetectionRequest): Promise<CreateDetectionResponse> {
return this.createDetectionUseCase.execute(request);
}
async getDetections(request: GetDetectionsRequest): Promise<GetDetectionsResponse> {
return this.getDetectionsUseCase.execute(request);
}
async getDetectionById(id: string, userId: string): Promise<Detection | null> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error('User not found');
}
const detection = await this.detectionRepository.findById(id);
if (!detection) {
return null;
}
if (!detection.canBeEditedBy(user) && !user.hasPermission('detection:read')) {
throw new Error('Access denied');
}
return detection;
}
}Phase 3: Extract Infrastructure Layer
Step 1: Repository Implementations
// src/infrastructure/repositories/GraphQLDetectionRepository.ts
import { Detection, DetectionStatus, DetectionSeverity } from '../../domain/entities/Detection';
import { DetectionRepository } from '../../domain/repositories/DetectionRepository';
import { graphqlClient } from '../external/graphqlClient';
export class GraphQLDetectionRepository implements DetectionRepository {
async findById(id: string): Promise<Detection | null> {
const query = `
query GetDetection($id: ID!) {
detection(id: $id) {
id
name
description
query
severity
status
mitreTechniques
createdBy
createdAt
updatedAt
}
}
`;
try {
const data = await graphqlClient.request(query, { id });
return this.mapToEntity(data.detection);
} catch (error) {
console.error('Error fetching detection:', error);
return null;
}
}
async findAll(): Promise<Detection[]> {
const query = `
query GetAllDetections {
detections {
id
name
description
query
severity
status
mitreTechniques
createdBy
createdAt
updatedAt
}
}
`;
try {
const data = await graphqlClient.request(query);
return data.detections.map((detection: any) => this.mapToEntity(detection));
} catch (error) {
console.error('Error fetching detections:', error);
return [];
}
}
async findByStatus(status: string): Promise<Detection[]> {
const query = `
query GetDetectionsByStatus($status: String!) {
detections(status: $status) {
id
name
description
query
severity
status
mitreTechniques
createdBy
createdAt
updatedAt
}
}
`;
try {
const data = await graphqlClient.request(query, { status });
return data.detections.map((detection: any) => this.mapToEntity(detection));
} catch (error) {
console.error('Error fetching detections by status:', error);
return [];
}
}
async findBySeverity(severity: string): Promise<Detection[]> {
const query = `
query GetDetectionsBySeverity($severity: String!) {
detections(severity: $severity) {
id
name
description
query
severity
status
mitreTechniques
createdBy
createdAt
updatedAt
}
}
`;
try {
const data = await graphqlClient.request(query, { severity });
return data.detections.map((detection: any) => this.mapToEntity(detection));
} catch (error) {
console.error('Error fetching detections by severity:', error);
return [];
}
}
async save(detection: Detection): Promise<Detection> {
const mutation = `
mutation CreateDetection($input: CreateDetectionInput!) {
createDetection(input: $input) {
id
name
description
query
severity
status
mitreTechniques
createdBy
createdAt
updatedAt
}
}
`;
const input = {
name: detection.name,
description: detection.description,
query: detection.query,
severity: detection.severity,
status: detection.status,
mitreTechniques: detection.mitreTechniques,
createdBy: detection.createdBy
};
try {
const data = await graphqlClient.request(mutation, { input });
return this.mapToEntity(data.createDetection);
} catch (error) {
console.error('Error creating detection:', error);
throw new Error('Failed to create detection');
}
}
async update(id: string, updates: Partial<Detection>): Promise<Detection> {
const mutation = `
mutation UpdateDetection($id: ID!, $input: UpdateDetectionInput!) {
updateDetection(id: $id, input: $input) {
id
name
description
query
severity
status
mitreTechniques
createdBy
createdAt
updatedAt
}
}
`;
const input = {
name: updates.name,
description: updates.description,
query: updates.query,
severity: updates.severity,
status: updates.status,
mitreTechniques: updates.mitreTechniques
};
try {
const data = await graphqlClient.request(mutation, { id, input });
return this.mapToEntity(data.updateDetection);
} catch (error) {
console.error('Error updating detection:', error);
throw new Error('Failed to update detection');
}
}
async delete(id: string): Promise<void> {
const mutation = `
mutation DeleteDetection($id: ID!) {
deleteDetection(id: $id)
}
`;
try {
await graphqlClient.request(mutation, { id });
} catch (error) {
console.error('Error deleting detection:', error);
throw new Error('Failed to delete detection');
}
}
private mapToEntity(data: any): Detection {
return new Detection(
data.id,
data.name,
data.description,
data.query,
data.severity as DetectionSeverity,
data.status as DetectionStatus,
data.mitreTechniques,
data.createdBy,
new Date(data.createdAt),
new Date(data.updatedAt)
);
}
}Step 2: External Services
// src/infrastructure/external/graphqlClient.ts
import { GraphQLClient } from 'graphql-request';
export const graphqlClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT || '/graphql', {
headers: {
'Content-Type': 'application/json',
},
});
// Add authentication token
graphqlClient.setHeader('Authorization', `Bearer ${localStorage.getItem('token')}`);
// src/infrastructure/external/MitreService.ts
export class MitreService {
async getTechniquesByKeywords(keywords: string[]): Promise<string[]> {
// This would typically call an external MITRE API
// For now, return mock data
const techniqueMap: Record<string, string[]> = {
'powershell': ['T1059.001'],
'registry': ['T1012'],
'network': ['T1043'],
'file': ['T1005'],
'process': ['T1057']
};
const techniques: string[] = [];
keywords.forEach(keyword => {
if (techniqueMap[keyword.toLowerCase()]) {
techniques.push(...techniqueMap[keyword.toLowerCase()]);
}
});
return [...new Set(techniques)]; // Remove duplicates
}
}Phase 4: Extract Presentation Layer
Step 1: React Components
// src/presentation/components/DetectionList.tsx
import React, { useEffect, useState } from 'react';
import { Detection } from '../../domain/entities/Detection';
import { DetectionApplicationService } from '../../application/services/DetectionApplicationService';
interface DetectionListProps {
detectionService: DetectionApplicationService;
userId: string;
}
export function DetectionList({ detectionService, userId }: DetectionListProps) {
const [detections, setDetections] = useState<Detection[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadDetections = async () => {
try {
setLoading(true);
const response = await detectionService.getDetections({ userId });
setDetections(response.detections);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load detections');
} finally {
setLoading(false);
}
};
loadDetections();
}, [detectionService, userId]);
if (loading) {
return <div>Loading detections...</div>;
}
if (error) {
return <div className="error">Error: {error}</div>;
}
return (
<div className="detection-list">
<h2>Detections ({detections.length})</h2>
{detections.map(detection => (
<div key={detection.id} className="detection-item">
<h3>{detection.name}</h3>
<p>{detection.description}</p>
<div className="detection-meta">
<span className={`severity ${detection.severity}`}>
{detection.severity}
</span>
<span className={`status ${detection.status}`}>
{detection.status}
</span>
</div>
{detection.mitreTechniques.length > 0 && (
<div className="mitre-techniques">
<strong>MITRE Techniques:</strong>
<ul>
{detection.mitreTechniques.map(technique => (
<li key={technique}>{technique}</li>
))}
</ul>
</div>
)}
</div>
))}
</div>
);
}
// src/presentation/components/CreateDetectionForm.tsx
import React, { useState } from 'react';
import { DetectionSeverity } from '../../domain/entities/Detection';
import { DetectionApplicationService } from '../../application/services/DetectionApplicationService';
interface CreateDetectionFormProps {
detectionService: DetectionApplicationService;
userId: string;
onDetectionCreated: () => void;
}
export function CreateDetectionForm({
detectionService,
userId,
onDetectionCreated
}: CreateDetectionFormProps) {
const [formData, setFormData] = useState({
name: '',
description: '',
query: '',
severity: DetectionSeverity.MEDIUM,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
await detectionService.createDetection({
...formData,
createdBy: userId,
});
setFormData({
name: '',
description: '',
query: '',
severity: DetectionSeverity.MEDIUM,
});
onDetectionCreated();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create detection');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="create-detection-form">
<h3>Create New Detection</h3>
{error && <div className="error">{error}</div>}
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
required
/>
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
id="description"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
required
/>
</div>
<div className="form-group">
<label htmlFor="query">Query</label>
<textarea
id="query"
value={formData.query}
onChange={(e) => setFormData(prev => ({ ...prev, query: e.target.value }))}
required
/>
</div>
<div className="form-group">
<label htmlFor="severity">Severity</label>
<select
id="severity"
value={formData.severity}
onChange={(e) => setFormData(prev => ({ ...prev, severity: e.target.value as DetectionSeverity }))}
>
<option value={DetectionSeverity.LOW}>Low</option>
<option value={DetectionSeverity.MEDIUM}>Medium</option>
<option value={DetectionSeverity.HIGH}>High</option>
<option value={DetectionSeverity.CRITICAL}>Critical</option>
</select>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Detection'}
</button>
</form>
);
}Step 2: Custom Hooks
// src/presentation/hooks/useDetections.ts
import { useState, useEffect } from 'react';
import { Detection } from '../../domain/entities/Detection';
import { DetectionApplicationService } from '../../application/services/DetectionApplicationService';
export function useDetections(
detectionService: DetectionApplicationService,
userId: string
) {
const [detections, setDetections] = useState<Detection[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadDetections = async () => {
try {
setLoading(true);
setError(null);
const response = await detectionService.getDetections({ userId });
setDetections(response.detections);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load detections');
} finally {
setLoading(false);
}
};
const createDetection = async (data: {
name: string;
description: string;
query: string;
severity: string;
}) => {
try {
const response = await detectionService.createDetection({
...data,
createdBy: userId,
});
setDetections(prev => [...prev, response.detection]);
return response;
} catch (err) {
throw err;
}
};
useEffect(() => {
loadDetections();
}, [detectionService, userId]);
return {
detections,
loading,
error,
loadDetections,
createDetection,
};
}Phase 5: Dependency Injection Setup
Step 1: Container Setup
// src/infrastructure/container/DIContainer.ts
import { DetectionApplicationService } from '../../application/services/DetectionApplicationService';
import { DetectionDomainService } from '../../domain/services/DetectionService';
import { GraphQLDetectionRepository } from '../repositories/GraphQLDetectionRepository';
import { GraphQLUserRepository } from '../repositories/GraphQLUserRepository';
export class DIContainer {
private static instance: DIContainer;
private services: Map<string, any> = new Map();
static getInstance(): DIContainer {
if (!DIContainer.instance) {
DIContainer.instance = new DIContainer();
}
return DIContainer.instance;
}
initialize(): void {
// Initialize repositories
const detectionRepository = new GraphQLDetectionRepository();
const userRepository = new GraphQLUserRepository();
// Initialize domain services
const detectionDomainService = new DetectionDomainService();
// Initialize application services
const detectionApplicationService = new DetectionApplicationService(
detectionRepository,
detectionDomainService,
userRepository
);
// Register services
this.services.set('detectionRepository', detectionRepository);
this.services.set('userRepository', userRepository);
this.services.set('detectionDomainService', detectionDomainService);
this.services.set('detectionApplicationService', detectionApplicationService);
}
get<T>(serviceName: string): T {
const service = this.services.get(serviceName);
if (!service) {
throw new Error(`Service ${serviceName} not found`);
}
return service as T;
}
}
// Initialize container
const container = DIContainer.getInstance();
container.initialize();
export { container };Step 2: Update Remix Routes
// app/routes/detections+/_index.tsx
import { type LoaderFunctionArgs } from 'react-router';
import { DetectionList } from '@/presentation/components/DetectionList';
import { CreateDetectionForm } from '@/presentation/components/CreateDetectionForm';
import { container } from '@/infrastructure/container/DIContainer';
export async function loader({ request, context }: LoaderFunctionArgs) {
// Get services from container
const detectionService = container.get<DetectionApplicationService>('detectionApplicationService');
// Load initial data if needed
return {
detectionService,
userId: 'current-user-id' // This would come from auth context
};
}
export default function DetectionsPage() {
const { detectionService, userId } = useLoaderData<typeof loader>();
return (
<div>
<h1>Detections</h1>
<CreateDetectionForm
detectionService={detectionService}
userId={userId}
onDetectionCreated={() => {
// Refresh the page or update state
window.location.reload();
}}
/>
<DetectionList
detectionService={detectionService}
userId={userId}
/>
</div>
);
}Clean Architecture Benefits
- Independence: Business logic is independent of external frameworks
- Testability: Easy to test business logic without external dependencies
- Flexibility: Easy to change external systems without affecting core logic
- Maintainability: Clear separation of concerns and dependencies
- Scalability: Easy to add new features following the same pattern
Migration Timeline
Week 1-2: Extract domain layer (entities, services, repository interfaces) Week 3-4: Extract application layer (use cases, application services) Week 5-6: Extract infrastructure layer (repository implementations, external services) Week 7-8: Extract presentation layer (React components, hooks) Week 9-10: Setup dependency injection and update Remix routes Week 11-12: Testing, optimization, and documentation
Implementation Considerations
- Dependency Direction: Dependencies should point inward toward the domain
- Interface Segregation: Use interfaces to define contracts between layers
- Dependency Injection: Use DI container to manage dependencies
- Testing Strategy: Test each layer independently with mocks
- Documentation: Document the architecture and layer responsibilities
Potential Challenges
- Complexity: Clean Architecture can be complex for simple applications
- Boilerplate: More code required for simple operations
- Learning Curve: Team needs to understand Clean Architecture principles
- Performance: Multiple layers might impact performance
- Over-engineering: Risk of over-engineering simple features
Success Metrics
- Test Coverage: Test coverage for each layer
- Dependency Direction: Percentage of dependencies pointing inward
- Code Reusability: Percentage of reusable domain logic
- Development Velocity: Time to implement new features
- Bug Reduction: Reduction in bugs due to better separation of concerns