Skip to main content
This guide documents the coding conventions, patterns, and best practices used throughout the InterviewGuide codebase. Following these conventions ensures consistency and maintainability.

Java Coding Conventions

Lombok Annotations

The project heavily uses Lombok to reduce boilerplate code. The most common annotations are:
Generates a constructor with required arguments (final fields and fields marked with @NonNull).
@Service
@RequiredArgsConstructor
public class ResumeUploadService {
    
    private final ResumeParseService parseService;
    private final FileStorageService storageService;
    private final ResumePersistenceService persistenceService;
    
    // Constructor automatically generated by Lombok
    // Enables constructor-based dependency injection
}
Benefits:
  • Eliminates constructor boilerplate
  • Works seamlessly with Spring’s constructor injection
  • Makes dependencies explicit and immutable
Automatically generates a logger field named log.
@Slf4j
@RestController
@RequiredArgsConstructor
public class ResumeController {
    
    @PostMapping("/api/resumes/upload")
    public Result<Map<String, Object>> uploadAndAnalyze(@RequestParam("file") MultipartFile file) {
        log.info("收到简历上传请求: {}, 大小: {} bytes", file.getOriginalFilename(), file.getSize());
        // ...
    }
}
Equivalent to:
private static final Logger log = LoggerFactory.getLogger(ResumeController.class);
Automatically generates getter and setter methods.
@Getter
public class BusinessException extends RuntimeException {
    private final Integer code;
    private final String message;
    
    // Lombok generates getCode() and getMessage()
}
Generates a constructor with all fields as parameters.
@Getter
@AllArgsConstructor
public enum ErrorCode {
    SUCCESS(200, "成功"),
    RESUME_NOT_FOUND(2001, "简历不存在"),
    RESUME_PARSE_FAILED(2002, "简历解析失败");
    
    private final Integer code;
    private final String message;
}

MapStruct for DTO Mapping

The project uses MapStruct for type-safe, compile-time object mapping between entities and DTOs.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ResumeMapper {
    
    /**
     * Entity to DTO conversion with field mapping
     */
    @Mapping(target = "filename", source = "originalFilename")
    @Mapping(target = "analyses", ignore = true)
    @Mapping(target = "interviews", ignore = true)
    ResumeDetailDTO toDetailDTOBasic(ResumeEntity entity);
    
    /**
     * DTO to Entity conversion with ignored fields
     */
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "resume", ignore = true)
    @Mapping(target = "analyzedAt", ignore = true)
    ResumeAnalysisEntity toAnalysisEntity(ResumeAnalysisResponse response);
    
    /**
     * Custom mapping with null handling
     */
    @Named("nullToZero")
    default int nullToZero(Integer value) {
        return value != null ? value : 0;
    }
    
    @Mapping(target = "contentScore", source = "contentScore", qualifiedByName = "nullToZero")
    ResumeAnalysisResponse.ScoreDetail toScoreDetail(ResumeAnalysisEntity entity);
}
Key Conventions:
  • Use componentModel = "spring" to make mappers Spring beans
  • Use unmappedTargetPolicy = ReportingPolicy.IGNORE for flexible mapping
  • Explicitly mark ignored fields with @Mapping(target = "field", ignore = true)
  • Use @Named methods for custom conversion logic
  • Handle JSON fields manually in service layer (MapStruct doesn’t handle JSON serialization)

Spring Framework Conventions

Use @RestController for REST API endpoints:
@Slf4j
@RestController
@RequiredArgsConstructor
public class ResumeController {
    
    private final ResumeUploadService uploadService;
    private final ResumeHistoryService historyService;
    
    @PostMapping(value = "/api/resumes/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @RateLimit(dimensions = {RateLimit.Dimension.GLOBAL, RateLimit.Dimension.IP}, count = 5)
    public Result<Map<String, Object>> uploadAndAnalyze(@RequestParam("file") MultipartFile file) {
        Map<String, Object> result = uploadService.uploadAndAnalyze(file);
        return Result.success(result);
    }
    
    @GetMapping("/api/resumes")
    public Result<List<ResumeListItemDTO>> getAllResumes() {
        List<ResumeListItemDTO> resumes = historyService.getAllResumes();
        return Result.success(resumes);
    }
}
Conventions:
  • Use @RestController (combines @Controller + @ResponseBody)
  • Use @RequiredArgsConstructor for dependency injection
  • Use @Slf4j for logging
  • Wrap all responses in Result<T>
  • Use descriptive method names
  • Add rate limiting with @RateLimit where appropriate

Exception Handling

The project uses a centralized exception handling strategy with custom exceptions and error codes.
@Getter
public class BusinessException extends RuntimeException {
    
    private final Integer code;
    private final String message;
    
    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
    }
    
    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.code = errorCode.getCode();
        this.message = message;
    }
}
Error Code Convention:
  • 1xxx: General errors (400, 401, 403, 404, 500)
  • 2xxx: Resume module errors
  • 3xxx: Interview module errors
  • 4xxx: Storage module errors
  • 5xxx: Export module errors
  • 6xxx: Knowledge base module errors
  • 7xxx: AI service errors
  • 8xxx: Rate limiting errors

Result Wrapper Pattern

All API endpoints return a standardized response format using the Result<T> wrapper.
@Getter
public class Result<T> {
    
    private final Integer code;
    private final String message;
    private final T data;
    
    private Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    
    // ========== 成功响应 ==========
    
    public static <T> Result<T> success() {
        return new Result<>(200, "success", null);
    }
    
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }
    
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(200, message, data);
    }
    
    // ========== 失败响应 ==========
    
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }
    
    public static <T> Result<T> error(ErrorCode errorCode) {
        return new Result<>(errorCode.getCode(), errorCode.getMessage(), null);
    }
}

Naming Conventions

Package Naming

  • Use lowercase, single words when possible
  • Use descriptive names: modules.resume, infrastructure.export
  • Group by feature/module, not by layer (prefer modules.resume.service over service.resume)

Class Naming

  • Controllers: {Module}Controller (e.g., ResumeController, InterviewController)
  • Services: {Module}{Action}Service (e.g., ResumeUploadService, ResumeParseService)
  • Repositories: {Entity}Repository (e.g., ResumeRepository, InterviewSessionRepository)
  • Entities: {Domain}Entity (e.g., ResumeEntity, InterviewSessionEntity)
  • DTOs: {Domain}DTO or {Domain}{Purpose}DTO (e.g., ResumeDetailDTO, ResumeListItemDTO)
  • Mappers: {Module}Mapper (e.g., ResumeMapper, InterviewMapper)
  • Exceptions: {Description}Exception (e.g., BusinessException, RateLimitExceededException)

Method Naming

  • Use descriptive, action-oriented names
  • Controllers: uploadAndAnalyze(), getResumeDetail(), deleteResume()
  • Services: parseResume(), analyzeResume(), saveAnalysis()
  • Repositories: Follow Spring Data JPA conventions (findById(), findByFileHash(), deleteById())

Variable Naming

  • Use camelCase for variables and parameters
  • Use descriptive names: resumeText, fileKey, analysisResult
  • Avoid single-letter variables except in loops
  • Use final for immutable variables when using Lombok’s @RequiredArgsConstructor

Documentation Standards

Javadoc Comments

Add Javadoc comments for:
  • All public classes
  • All public methods (especially in services)
  • Complex private methods
  • Enum values
/**
 * 简历上传服务
 * 处理简历上传、解析的业务逻辑
 * AI 分析改为异步处理,通过 Redis Stream 实现
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ResumeUploadService {
    
    /**
     * 上传并分析简历(异步)
     *
     * @param file 简历文件
     * @return 上传结果(分析将异步进行)
     */
    public Map<String, Object> uploadAndAnalyze(MultipartFile file) {
        // ...
    }
}

Code Comments

  • Use inline comments sparingly (code should be self-documenting)
  • Add comments for complex business logic
  • Explain why, not what
  • Use numbered steps for multi-step processes
public Map<String, Object> uploadAndAnalyze(MultipartFile file) {
    // 1. 验证文件
    fileValidationService.validateFile(file, MAX_FILE_SIZE, "简历");
    
    // 2. 验证文件类型
    String contentType = parseService.detectContentType(file);
    validateContentType(contentType);
    
    // 3. 检查简历是否已存在(去重)
    Optional<ResumeEntity> existingResume = persistenceService.findExistingResume(file);
    if (existingResume.isPresent()) {
        return handleDuplicateResume(existingResume.get());
    }
    
    // 4-8. Process and return result...
}

Testing Conventions

Testing is currently minimal in the codebase. Future contributions should include comprehensive unit and integration tests.
Recommended practices:
  • Use JUnit 5 for unit tests
  • Use @SpringBootTest for integration tests
  • Use Mockito for mocking dependencies
  • Follow naming convention: {ClassName}Test (e.g., ResumeUploadServiceTest)
  • Use descriptive test method names: uploadAndAnalyze_WhenValidFile_ShouldReturnSuccess()

Frontend Conventions

TypeScript

  • Use strict type checking
  • Define interfaces for all API request/response types
  • Use TypeScript features (union types, generics, type guards)
// types/resume.ts
export interface ResumeListItem {
  id: number;
  filename: string;
  fileSize: number;
  uploadedAt: string;
  latestScore: number | null;
  lastAnalyzedAt: string | null;
  interviewCount: number;
}

export interface ResumeDetail extends ResumeListItem {
  analyses: AnalysisHistory[];
  interviews: InterviewSummary[];
}

React Components

  • Use functional components with hooks
  • Use TypeScript for props
  • Keep components small and focused
  • Extract reusable logic into custom hooks
interface ResumeCardProps {
  resume: ResumeListItem;
  onDelete: (id: number) => void;
  onView: (id: number) => void;
}

export const ResumeCard: React.FC<ResumeCardProps> = ({ resume, onDelete, onView }) => {
  return (
    <div className="border rounded-lg p-4">
      <h3 className="font-semibold">{resume.filename}</h3>
      <p className="text-sm text-gray-600">Score: {resume.latestScore ?? 'N/A'}</p>
      <div className="mt-4 flex gap-2">
        <button onClick={() => onView(resume.id)}>View</button>
        <button onClick={() => onDelete(resume.id)}>Delete</button>
      </div>
    </div>
  );
};

API Client

  • Centralize API calls in src/api/ directory
  • Use axios for HTTP requests
  • Handle errors consistently
// api/resume.ts
import axios from 'axios';
import type { ResumeListItem, ResumeDetail } from '@/types/resume';

const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';

export const resumeApi = {
  uploadResume: async (file: File) => {
    const formData = new FormData();
    formData.append('file', file);
    const response = await axios.post(`${API_BASE}/api/resumes/upload`, formData);
    return response.data;
  },

  getResumeList: async (): Promise<ResumeListItem[]> => {
    const response = await axios.get(`${API_BASE}/api/resumes`);
    return response.data.data;
  },

  getResumeDetail: async (id: number): Promise<ResumeDetail> => {
    const response = await axios.get(`${API_BASE}/api/resumes/${id}/detail`);
    return response.data.data;
  },
};

Project Structure

Understand the codebase organization

Building the Project

Build and run commands

Backend Services

Deep dive into service implementations

Redis Streams

Learn about async processing patterns

Build docs developers (and LLMs) love