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

  1. Independence: Business logic is independent of external frameworks
  2. Testability: Easy to test business logic without external dependencies
  3. Flexibility: Easy to change external systems without affecting core logic
  4. Maintainability: Clear separation of concerns and dependencies
  5. 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

  1. Dependency Direction: Dependencies should point inward toward the domain
  2. Interface Segregation: Use interfaces to define contracts between layers
  3. Dependency Injection: Use DI container to manage dependencies
  4. Testing Strategy: Test each layer independently with mocks
  5. Documentation: Document the architecture and layer responsibilities

Potential Challenges

  1. Complexity: Clean Architecture can be complex for simple applications
  2. Boilerplate: More code required for simple operations
  3. Learning Curve: Team needs to understand Clean Architecture principles
  4. Performance: Multiple layers might impact performance
  5. 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