Skip to content

Coding Guidelines

Jihyeok edited this page Aug 21, 2025 · 2 revisions

Coding Guidelines

This document establishes coding standards and best practices for OpenContext development. These guidelines ensure code consistency, maintainability, and security across the entire codebase.

General Principles

Code Quality Standards

  • Clarity over cleverness: Write code that is easy to read and understand
  • Explicit over implicit: Make intentions clear through explicit naming and logic
  • Fail-fast approach: Detect and handle errors as early as possible
  • Separation of concerns: Each component should have a single, well-defined responsibility

Security-First Development

  • Input validation: Never trust external input, validate at boundaries
  • Error handling: Provide meaningful errors without exposing sensitive information
  • Audit logging: Log all significant actions for security and debugging
  • Dependency management: Regularly update dependencies and scan for vulnerabilities

Backend Development (Java/Spring Boot)

Project Structure

All backend code follows this standardized package structure:

src/main/java/com/opencontext/
├── config/              # Spring configuration classes
├── common/              # Shared response objects and constants
├── controller/          # REST API endpoints
├── service/             # Business logic implementations
├── repository/          # Data access layer
│   └── querydsl/        # QueryDSL custom implementations
├── entity/              # JPA entities
├── dto/                 # Data transfer objects
├── enums/               # Enumeration classes
├── exception/           # Custom exceptions and handlers
├── security/            # Spring Security configurations
├── pipeline/            # Document processing pipelines
│   ├── orchestrator/    # Pipeline coordination
│   └── step/            # Individual processing steps
└── util/                # Pure utility functions

Dependency Injection

Use constructor injection exclusively:

@Service
@RequiredArgsConstructor
public class DocumentService {
    private final DocumentRepository documentRepository;
    private final EmbeddingService embeddingService;
    
    // Constructor injection via Lombok @RequiredArgsConstructor
}

Prohibited patterns:

// Never use field injection
@Autowired
private DocumentRepository documentRepository;

// Avoid setter injection unless absolutely necessary
@Autowired
public void setDocumentRepository(DocumentRepository repo) {
    this.documentRepository = repo;
}

Transaction Management

Service-level transaction configuration:

@Service
@Transactional(readOnly = true)  // Default for all methods
@RequiredArgsConstructor
public class DocumentService {

    @Transactional  // Override for write operations
    public SourceDocument createDocument(CreateDocumentRequest request) {
        // Implementation
    }
    
    // Read operations inherit readOnly = true
    public List<SourceDocument> getDocuments() {
        // Implementation
    }
}

Exception Handling

Global exception handling:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<CommonResponse<Void>> handleBusinessException(BusinessException ex) {
        log.warn("Business exception: {}", ex.getMessage());
        return ResponseEntity
            .status(ex.getHttpStatus())
            .body(CommonResponse.error(ex.getErrorCode(), ex.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<CommonResponse<Void>> handleValidationException(
            MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.joining(", "));
        
        return ResponseEntity.badRequest()
            .body(CommonResponse.error(ErrorCode.VALIDATION_FAILED, message));
    }
}

Custom exception structure:

public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;
    private final HttpStatus httpStatus;

    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
        this.httpStatus = errorCode.getHttpStatus();
    }
}

Entity Design

Immutable entities with builder pattern:

@Entity
@Table(name = "source_documents")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class SourceDocument {
    
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(name = "UUID", strategy = "uuid2")
    @Column(columnDefinition = "UUID")
    private UUID id;
    
    @Column(nullable = false)
    private String originalFilename;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private IngestionStatus ingestionStatus;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    // Business methods, not setters
    public void markAsCompleted() {
        this.ingestionStatus = IngestionStatus.COMPLETED;
        this.lastIngestedAt = LocalDateTime.now();
    }
    
    public boolean isProcessing() {
        return ingestionStatus.isProcessing();
    }
}

DTO Patterns

Request DTOs with validation:

@Schema(description = "Request to upload a new document")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UploadDocumentRequest {
    
    @Schema(description = "Document file", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "File is required")
    private MultipartFile file;
    
    @Schema(description = "Document description", example = "Spring Security Reference")
    @Size(max = 500, message = "Description must not exceed 500 characters")
    private String description;
}

Response DTOs:

@Schema(description = "Document information response")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class SourceDocumentResponse {
    
    @Schema(description = "Document unique identifier")
    private UUID id;
    
    @Schema(description = "Original filename")
    private String originalFilename;
    
    @Schema(description = "Current processing status")
    private IngestionStatus ingestionStatus;
    
    @Schema(description = "File size in bytes")
    private Long fileSize;
    
    @Schema(description = "Creation timestamp")
    private LocalDateTime createdAt;
    
    public static SourceDocumentResponse from(SourceDocument entity) {
        return SourceDocumentResponse.builder()
            .id(entity.getId())
            .originalFilename(entity.getOriginalFilename())
            .ingestionStatus(entity.getIngestionStatus())
            .fileSize(entity.getFileSize())
            .createdAt(entity.getCreatedAt())
            .build();
    }
}

Repository Patterns

Standard repository interface:

@Repository
public interface SourceDocumentRepository extends JpaRepository<SourceDocument, UUID>, 
                                                   SourceDocumentRepositoryCustom {
    
    List<SourceDocument> findByIngestionStatus(IngestionStatus status);
    
    Optional<SourceDocument> findByFileChecksum(String checksum);
    
    @Query("SELECT COUNT(s) FROM SourceDocument s WHERE s.ingestionStatus = :status")
    long countByStatus(@Param("status") IngestionStatus status);
}

Custom repository implementation:

@Repository
@RequiredArgsConstructor
public class SourceDocumentRepositoryImpl implements SourceDocumentRepositoryCustom {
    
    private final JPAQueryFactory queryFactory;
    
    @Override
    public Page<SourceDocument> findWithComplexCriteria(SearchCriteria criteria, Pageable pageable) {
        QSourceDocument document = QSourceDocument.sourceDocument;
        
        BooleanBuilder builder = new BooleanBuilder();
        
        if (criteria.getFilename() != null) {
            builder.and(document.originalFilename.containsIgnoreCase(criteria.getFilename()));
        }
        
        if (criteria.getStatus() != null) {
            builder.and(document.ingestionStatus.eq(criteria.getStatus()));
        }
        
        List<SourceDocument> results = queryFactory
            .selectFrom(document)
            .where(builder)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(document.createdAt.desc())
            .fetch();
            
        long total = queryFactory
            .selectFrom(document)
            .where(builder)
            .fetchCount();
            
        return new PageImpl<>(results, pageable, total);
    }
}

Service Layer Guidelines

Clean service implementation:

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class DocumentService {
    
    private final SourceDocumentRepository documentRepository;
    private final FileStorageService fileStorageService;
    private final IngestionOrchestrator ingestionOrchestrator;
    
    @Transactional
    public SourceDocumentResponse uploadDocument(UploadDocumentRequest request) {
        validateUploadRequest(request);
        
        String checksum = calculateChecksum(request.getFile());
        checkForDuplicates(checksum);
        
        String storagePath = fileStorageService.store(request.getFile());
        
        SourceDocument document = SourceDocument.builder()
            .originalFilename(request.getFile().getOriginalFilename())
            .fileStoragePath(storagePath)
            .fileChecksum(checksum)
            .fileSize(request.getFile().getSize())
            .ingestionStatus(IngestionStatus.PENDING)
            .build();
            
        SourceDocument saved = documentRepository.save(document);
        
        // Trigger async processing
        ingestionOrchestrator.processDocument(saved.getId());
        
        log.info("Document uploaded successfully: id={}, filename={}", 
                saved.getId(), saved.getOriginalFilename());
                
        return SourceDocumentResponse.from(saved);
    }
    
    private void validateUploadRequest(UploadDocumentRequest request) {
        if (request.getFile().isEmpty()) {
            throw new BusinessException(ErrorCode.VALIDATION_FAILED, "File cannot be empty");
        }
        
        if (!isValidFileType(request.getFile())) {
            throw new BusinessException(ErrorCode.UNSUPPORTED_MEDIA_TYPE, 
                "Only PDF and Markdown files are supported");
        }
    }
}

Logging Standards

Structured logging with context:

@Slf4j
public class DocumentService {
    
    public void processDocument(UUID documentId) {
        log.info("Starting document processing: documentId={}", documentId);
        
        try {
            // Processing logic
            log.debug("Processing step completed: documentId={}, step=parsing", documentId);
            
        } catch (Exception e) {
            log.error("Document processing failed: documentId={}, error={}", 
                     documentId, e.getMessage(), e);
            throw new BusinessException(ErrorCode.INGESTION_PIPELINE_FAILED, 
                                      "Document processing failed");
        }
        
        log.info("Document processing completed: documentId={}", documentId);
    }
}

Testing Standards

Unit test structure:

@ExtendWith(MockitoExtension.class)
class DocumentServiceTest {
    
    @Mock
    private SourceDocumentRepository documentRepository;
    
    @Mock
    private FileStorageService fileStorageService;
    
    @InjectMocks
    private DocumentService documentService;
    
    @Test
    @DisplayName("Should upload document successfully when valid file provided")
    void shouldUploadDocumentSuccessfully() {
        // Given
        MultipartFile file = createMockFile("test.pdf", "application/pdf");
        UploadDocumentRequest request = UploadDocumentRequest.builder()
            .file(file)
            .build();
            
        when(fileStorageService.store(file)).thenReturn("stored/path/test.pdf");
        when(documentRepository.save(any())).thenReturn(createMockDocument());
        
        // When
        SourceDocumentResponse response = documentService.uploadDocument(request);
        
        // Then
        assertThat(response).isNotNull();
        assertThat(response.getOriginalFilename()).isEqualTo("test.pdf");
        assertThat(response.getIngestionStatus()).isEqualTo(IngestionStatus.PENDING);
        
        verify(documentRepository).save(any(SourceDocument.class));
        verify(fileStorageService).store(file);
    }
    
    @Test
    @DisplayName("Should throw exception when duplicate file uploaded")
    void shouldThrowExceptionForDuplicateFile() {
        // Given
        MultipartFile file = createMockFile("test.pdf", "application/pdf");
        UploadDocumentRequest request = UploadDocumentRequest.builder()
            .file(file)
            .build();
            
        when(documentRepository.findByFileChecksum(anyString()))
            .thenReturn(Optional.of(createMockDocument()));
        
        // When & Then
        assertThatThrownBy(() -> documentService.uploadDocument(request))
            .isInstanceOf(BusinessException.class)
            .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DUPLICATE_FILE_UPLOADED);
    }
}

Integration test with TestContainers:

@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class DocumentServiceIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
            .withDatabaseName("opencontext_test")
            .withUsername("test")
            .withPassword("test");
    
    @Autowired
    private DocumentService documentService;
    
    @Autowired
    private SourceDocumentRepository documentRepository;
    
    @Test
    @Transactional
    @Rollback
    void shouldPersistDocumentCorrectly() {
        // Test implementation with real database
    }
}

Frontend Development (React/TypeScript)

Component Structure

Functional components with TypeScript:

interface DocumentListProps {
  documents: SourceDocument[];
  onDocumentSelect: (document: SourceDocument) => void;
  loading?: boolean;
}

export const DocumentList: React.FC<DocumentListProps> = ({ 
  documents, 
  onDocumentSelect, 
  loading = false 
}) => {
  if (loading) {
    return <LoadingSpinner />;
  }

  return (
    <div className="space-y-4">
      {documents.map((document) => (
        <DocumentCard
          key={document.id}
          document={document}
          onClick={() => onDocumentSelect(document)}
        />
      ))}
    </div>
  );
};

API Client Structure

Type-safe API client:

export interface ApiResponse<T> {
  success: boolean;
  data: T | null;
  message: string;
  errorCode: string | null;
  timestamp: string;
}

export class OpenContextApiClient {
  constructor(private baseUrl: string, private apiKey: string) {}

  async uploadDocument(file: File): Promise<SourceDocumentResponse> {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch(`${this.baseUrl}/api/v1/sources/upload`, {
      method: 'POST',
      headers: {
        'X-API-KEY': this.apiKey,
      },
      body: formData,
    });

    if (!response.ok) {
      throw new ApiError(`Upload failed: ${response.statusText}`);
    }

    const apiResponse: ApiResponse<SourceDocumentResponse> = await response.json();
    
    if (!apiResponse.success) {
      throw new ApiError(apiResponse.message, apiResponse.errorCode);
    }

    return apiResponse.data!;
  }
}

State Management

Zustand store pattern:

interface AuthState {
  apiKey: string | null;
  isAuthenticated: boolean;
  setApiKey: (key: string) => void;
  clearAuth: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      apiKey: null,
      isAuthenticated: false,
      
      setApiKey: (key: string) =>
        set({ apiKey: key, isAuthenticated: true }),
        
      clearAuth: () =>
        set({ apiKey: null, isAuthenticated: false }),
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({ apiKey: state.apiKey }),
    }
  )
);

MCP Adapter Development (Node.js/TypeScript)

Protocol Implementation

MCP tool handler:

interface MCPRequest {
  tool_name: string;
  parameters: Record<string, unknown>;
}

interface MCPResponse {
  [key: string]: unknown;
}

interface MCPError {
  error: {
    code: string;
    message: string;
  };
}

export class MCPAdapter {
  constructor(private coreApiUrl: string) {}

  async handleToolCall(request: MCPRequest): Promise<MCPResponse | MCPError> {
    try {
      switch (request.tool_name) {
        case 'find_knowledge':
          return await this.handleFindKnowledge(request.parameters);
        case 'get_content':
          return await this.handleGetContent(request.parameters);
        default:
          return this.createError('UNKNOWN_TOOL', `Unknown tool: ${request.tool_name}`);
      }
    } catch (error) {
      return this.createError('EXTERNAL_SERVICE_UNAVAILABLE', 
                             'Failed to connect to OpenContext core service');
    }
  }

  private async handleFindKnowledge(params: Record<string, unknown>): Promise<MCPResponse> {
    const query = params.query as string;
    const topK = (params.topK as number) || 5;

    const response = await fetch(
      `${this.coreApiUrl}/api/v1/search?query=${encodeURIComponent(query)}&topK=${topK}`
    );

    const apiResponse = await response.json();
    return apiResponse.data;
  }

  private createError(code: string, message: string): MCPError {
    return {
      error: { code, message }
    };
  }
}

Git and Version Control

Commit Message Format

Use Conventional Commits specification:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes
  • refactor: Code refactoring
  • test: Test additions or modifications
  • chore: Build process or auxiliary tool changes

Examples:

feat(search): implement hybrid search with BM25 and vector similarity

Add Elasticsearch integration with Korean Nori analyzer support.
Combines keyword and semantic search for improved relevance.

Closes #123
fix(api): resolve token counting issue in content retrieval

The tiktoken tokenizer was not properly handling Unicode characters,
causing incorrect token limits in get_content responses.

Fixes #156

Branch Naming

Follow Git Flow convention:

  • feature/issue-number-brief-description
  • fix/issue-number-brief-description
  • hotfix/issue-number-brief-description

Examples:

  • feature/25-implement-document-search-api
  • fix/42-resolve-elasticsearch-connection-timeout

Pull Request Guidelines

PR Title Format:

<type>(scope): brief description of changes

PR Template:

## Summary
Brief description of what this PR accomplishes.

## Changes Made
- List of specific changes
- Another change
- Third change

## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests pass
- [ ] Manual testing completed

## Related Issues
Closes #issue-number

## Deployment Notes
Any special deployment considerations.

Security Guidelines

Input Validation

Always validate at boundaries:

@PostMapping("/upload")
public ResponseEntity<CommonResponse<SourceDocumentResponse>> uploadDocument(
        @Valid @RequestBody UploadDocumentRequest request) {
    
    // Additional validation beyond annotations
    if (!SecurityUtils.isValidFilename(request.getFile().getOriginalFilename())) {
        throw new BusinessException(ErrorCode.VALIDATION_FAILED, 
                                  "Invalid filename characters");
    }
    
    return ResponseEntity.accepted()
        .body(CommonResponse.success(documentService.uploadDocument(request)));
}

Sensitive Data Handling

Never log sensitive information:

// Good
log.info("API key validation failed for request: userId={}", userId);

// Bad - exposes sensitive data
log.info("API key validation failed: key={}", apiKey);

Secure configuration:

@ConfigurationProperties(prefix = "opencontext")
@Data
public class OpenContextProperties {
    
    @ToString.Exclude  // Prevent accidental logging
    private String apiKey;
    
    private SearchProperties search = new SearchProperties();
}

Dependency Security

Regular security scanning:

# Gradle dependency check
./gradlew dependencyCheckAnalyze

# Update dependencies
./gradlew dependencyUpdates

# Audit npm packages
npm audit
npm audit fix

Performance Guidelines

Database Optimization

Use appropriate indexes:

-- Index for common queries
CREATE INDEX idx_source_documents_status_created 
ON source_documents(ingestion_status, created_at DESC);

-- Partial index for active documents
CREATE INDEX idx_source_documents_active 
ON source_documents(id) 
WHERE ingestion_status NOT IN ('COMPLETED', 'ERROR');

QueryDSL optimization:

// Efficient pagination with exists check
public Page<SourceDocument> findDocumentsWithChunks(Pageable pageable) {
    QSourceDocument doc = QSourceDocument.sourceDocument;
    QDocumentChunk chunk = QDocumentChunk.documentChunk;
    
    List<SourceDocument> results = queryFactory
        .selectFrom(doc)
        .where(JPAExpressions.selectOne()
                .from(chunk)
                .where(chunk.sourceDocumentId.eq(doc.id))
                .exists())
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();
        
    // Use separate count query for performance
    Long total = queryFactory
        .select(doc.count())
        .from(doc)
        .where(JPAExpressions.selectOne()
                .from(chunk)
                .where(chunk.sourceDocumentId.eq(doc.id))
                .exists())
        .fetchOne();
        
    return new PageImpl<>(results, pageable, total);
}

Caching Strategy

Application-level caching:

@Service
@CacheConfig(cacheNames = "documents")
public class DocumentService {
    
    @Cacheable(key = "#id")
    public SourceDocumentResponse getDocument(UUID id) {
        return documentRepository.findById(id)
            .map(SourceDocumentResponse::from)
            .orElseThrow(() -> new BusinessException(
                ErrorCode.SOURCE_DOCUMENT_NOT_FOUND, 
                "Document not found"));
    }
    
    @CacheEvict(key = "#id")
    public void invalidateDocument(UUID id) {
        // Cache will be cleared automatically
    }
}

These coding guidelines ensure consistent, secure, and maintainable code across the OpenContext project. All contributors must follow these standards to maintain code quality and team productivity.