-
Notifications
You must be signed in to change notification settings - Fork 0
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.
- 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
- 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
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
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;
}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
}
}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();
}
}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();
}
}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();
}
}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);
}
}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");
}
}
}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);
}
}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
}
}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>
);
};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!;
}
}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 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 }
};
}
}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
Follow Git Flow convention:
feature/issue-number-brief-descriptionfix/issue-number-brief-descriptionhotfix/issue-number-brief-description
Examples:
feature/25-implement-document-search-apifix/42-resolve-elasticsearch-connection-timeout
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.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)));
}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();
}Regular security scanning:
# Gradle dependency check
./gradlew dependencyCheckAnalyze
# Update dependencies
./gradlew dependencyUpdates
# Audit npm packages
npm audit
npm audit fixUse 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);
}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.
Project Management
Contributing
Documentation