Skip to content

Development Style Guide

Zihlu Wang edited this page Oct 20, 2025 · 2 revisions

1 Introduction

This document outlines the coding standards and best practices for developing Spring Boot 3.5 web applications within our organisation. Adherence to these guidelines ensures code quality, maintainability, and consistency across all projects. This standard specifically addresses projects leveraging Spring Boot 3.5, JDK 17, Gradle for backend, and React 19 with Ant Design, Redux, and Axios for frontend development.

2 General Principles

  • Clarity & Readability: Code must be easy to read and understand. Prioritise clear, self-documenting code over clever, concise but obscure solutions.
  • Consistency: Maintain consistent coding styles, naming conventions, and architectural patterns across the project.
  • Modularity: Design components to be loosely coupled and highly cohesive, promoting reusability and easier testing.
  • Testability: Write code that is inherently testable.
  • Security by Design: Embed security considerations into every phase of development.
  • Performance Awareness: Consider performance implications for critical sections of code and API endpoints.

3. Backend Standards (Spring Boot, JDK 17, Gradle)

3.1. Project Structure

The backend application (helix-server) follows a structured Gradle project layout. The core application logic resides within the uk.co.organisation.projectname package (adjusted to com.onixbyte.helix for your specific structure), with a clear separation of concerns into various sub-packages.

source/repos/helix/helix-server  
├── build.gradle.kts           // Main Gradle build script for the project
├── config                     // External configuration directory
│   ├── application-dev.yml    // Application properties for development environment
│   └── application-prod.yml.example // Example production properties (to be copied and configured)
├── database                   // Database-related files
│   └── init.d                 // Initialisation scripts for the database
│       └── init-en_GB.sql     // SQL script for database schema and initial data (British English locale)
├── gradle                     // Gradle wrapper and configuration files
│   ├── libs.versions.toml     // Centralised dependency versions (Gradle Version Catalogs)
│   └── wrapper                // Gradle wrapper files
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties          // Project-specific Gradle properties
├── gradlew                    // Gradle wrapper executable (Linux/macOS)
├── gradlew.bat                // Gradle wrapper executable (Windows)
├── settings.gradle.kts        // Gradle settings for multi-project builds (if applicable)
└── src
    ├── main
    │   ├── java
    │   │   └── com/onixbyte/helix // Root package for the application
    │   │       ├── HelixApplication.java    // Main Spring Boot application entry point
    │   │       ├── config                   // Spring configuration classes
    │   │       ├── constant                 // Classes defining application-wide constants
    │   │       ├── controller               // REST API endpoints
    │   │       ├── domain                   // Core domain models and related types
    │   │       │   ├── common               // Common domain objects/utility classes
    │   │       │   ├── database             // Database-specific domain objects (e.g., enums for DB types)
    │   │       │   ├── entity               // JPA/MyBatis entities representing database tables
    │   │       │   ├── model                // General purpose domain models not directly tied to DB entities
    │   │       │   ├── view                 // DTOs specifically for read operations (e.g. data projections)
    │   │       │   └── web                  // DTOs specifically for web request/response bodies
    │   │       │       ├── request          // Request DTOs
    │   │       │       └── response         // Response DTOs
    │   │       ├── exception                // Custom application-specific exceptions
    │   │       ├── extension                // Extension points or custom functionality
    │   │       │   ├── jackson              // Jackson serialisation/deserialisation extensions
    │   │       │   └── redis                // Redis-related extensions
    │   │       │       └── serializer
    │   │       ├── filter                   // Servlet filters or Spring Security filters
    │   │       ├── manager                  // Business logic orchestrators, often coordinating multiple services or repositories
    │   │       ├── processor                // Generic processing components or business workflows
    │   │       ├── properties               // Classes for type-safe configuration properties (`@ConfigurationProperties`)
    │   │       ├── repository               // Data access layer (MyBatis Mapper interfaces)
    │   │       ├── security                 // Spring Security specific components
    │   │       │   ├── authentication       // Custom authentication mechanisms
    │   │       │   └── provider             // Custom authentication providers
    │   │       ├── service                  // Core business logic (transactional layer)
    │   │       ├── utils                    // General utility classes
    │   │       └── validation               // Custom validation logic
    │   │           └── group                // Validation groups for different contexts (e.g., creation, update)
    │   └── resources
    │       ├── application.yml                 // Default application properties
    │       └── repository                      // MyBatis XML mapper files
    └── test
        └── java
            └── com/onixbyte/helix
                └── HelixApplicationTests.java // Spring Boot integration tests

Key Observations and Specific Directives:

  • External config Directory: Environmental configurations (application-dev.yml, application-prod.yml.example) are managed in a top-level config directory, separate from src/main/resources. This promotes environment-specific property management, allowing different configurations to be mounted or linked at deployment time.
  • Database Initialisation: The database/init.d directory is reserved for SQL scripts, specifically for database schema initialisation (init-en_GB.sql), which is crucial for environment setup and CI/CD pipelines. This structure implies a "schema-first" or "code-driven schema evolution" approach.
  • MyBatis Integration: The src/main/java/.../repository package contains MyBatis mapper interfaces, while the actual SQL definitions are located in src/main/resources/repository/*.xml files. This separation is key to maintaining clean code while leveraging MyBatis's powerful XML mapping capabilities.
  • domain Package Granularity:
    • domain.entity: Reserved for classes that directly map to database tables (POJOs for MyBatis).
    • domain.model: For more generic domain objects or aggregate roots not directly mapping one-to-one with a single table.
    • domain.view: Specifically for DTOs used to present data in read-only scenarios (e.g., query results, report structures).
    • domain.web.request / domain.web.response: Clearly segregated DTOs for incoming API requests and outgoing API responses, strictly adhering to the API contract and decoupling from internal domain entities.
  • manager and processor Packages: These packages suggest a layered architecture where "managers" orchestrate operations involving multiple services or repositories, while "processors" might handle specific aspects of a business process. It is imperative that the responsibilities of classes within these packages are clearly defined and documented to prevent an 'anemic domain model' or 'god object' anti-patterns.
  • security Package: This sub-package houses custom Spring Security components beyond the initial configuration, such as custom authentication types and providers, indicating a tailored security implementation.
  • properties Package: This is well-placed for custom @ConfigurationProperties classes, promoting type-safe access to application settings defined in YML files.
  • extension Package: This is a flexible area for application-specific extensions, such as custom Jackson serialisers or Redis customisations. It should be used judiciously to prevent a 'miscellaneous' dumping ground.

3.2. Java Language & Coding Style

  • JDK 17 Features: Leverage modern JDK 17 features where appropriate to write cleaner and more efficient code (e.g., records, switch expressions, text blocks).
  • Naming Conventions:
    • Classes: PascalCase (e.g., UserService, OrderController).
    • Methods: camelCase (e.g., getUserById, saveOrder).
    • Variables: camelCase (e.g., userName, statusCode).
    • Constants: SCREAMING_SNAKE_CASE (e.g., DEFAULT_PAGE_SIZE).
  • Immutability: Favour immutability for domain objects and DTOs where possible, using records or immutable classes to reduce side effects and improve thread safety.
  • Optional: Use Optional<T> to explicitly handle the potential absence of a value, avoiding NullPointerExceptions.
  • Streams API: Prefer the Java Streams API for collection processing, promoting functional and declarative programming.
  • Exception Handling:
    • Use specific exceptions. Avoid catching generic Exception.
    • For unrecoverable errors, throw runtime exceptions.
    • For recoverable errors, define custom checked exceptions if business logic dictates.
    • Utilise Spring's @ControllerAdvice and @ExceptionHandler for centralised global exception handling and consistent API error responses.
  • Code Review: All backend code must undergo a thorough manual code review prior to merging, specifically using the GitFlow principle. IntelliJ IDEA's integrated code analysis tools should be used as a first pass.

3.3 Spring Boot Best Practices

  • Layered Architecture (MVC with Manager Layer): Our backend application adheres to a strict multi-layered architecture, ensuring a clear separation of concerns and promoting maintainability and testability. The layers and their responsibilities are as follows:

    • Controller Layer: Located in the controller package. Responsible for exposing RESTful APIs, handling HTTP requests, and mapping request parameters/bodies to service layer calls. Controllers should be thin, primarily focusing on input validation (using DTOs) and orchestrating calls to the Service layer.
    • Service Layer: Located in the service package. This layer encapsulates the core business logic. Services provide a consumable API to the Controller layer, abstracting business processes and transaction management. Services coordinate calls to the Manager layer to perform business operations.
    • Manager Layer: Located in the manager package. This layer provides atomic business operations that can be composed by the Service layer. Managers often deal with more complex business logic that might involve interactions with multiple repositories or other external systems at a granular level.
    • Repository Layer (MyBatis): Located in the repository package and src/main/resources/repository (for XML mappers). This layer is responsible for providing atomic database interaction operations.
  • Layer Communication Policy:

    • Components within each layer may only invoke components in the layer directly beneath it.
    • Cross-layer invocation (e.g., Controller calling Manager directly, Service calling Repository directly) is strictly forbidden.
    • Upward invocation (e.g., Service calling Controller) is strictly forbidden.
    • Lateral invocation (e.g., Service A calling Service B for a different domain) should be carefully considered and typically indicates a need to refactor shared logic into a Manager or a dedicated Service for that shared concern.
  • Configuration: Prefer application.yml for configuration properties over application.properties for better readability and hierarchical structure. Use @ConfigurationProperties for type-safe configuration.

  • Dependency Injection: Use constructor injection for all dependencies. Avoid @Autowired on fields or setters as it makes testing harder and hides dependencies.

  • Services: Annotate business logic classes with @Service. Keep services thin and focused on orchestrating domain operations, which often involve interacting with MyBatis repositories for data access, rather than containing direct data access logic.

  • Repositories (MyBatis):

    • MyBatis is used for database operations. Repository (mapper) interfaces in the repository package define the data access contract.
    • The corresponding SQL definitions are managed in XML mapper files located in src/main/resources/repository.
    • MyBatis Method Naming Conventions:
      • For querying a list of data: Methods must start with selectListBy, immediately followed by criteria (e.g., selectListByUserId, selectListByDepartmentIdAndStatus). These methods must also include a PageRequest parameter for pagination.
      • For querying a single data record: Methods must start with selectOne (e.g., selectOneById, selectOneByUsername).
      • For saving new data: Methods must be named save (e.g., saveUser, saveDepartment).
      • For updating existing data: Methods must be named update (e.g., updateUser, updateDepartmentById).
      • For deleting data: Methods must start with deleteBy, clearly specifying the criteria for deletion (e.g., deleteById, deleteByUserId).
  • Controllers:

    • Annotate REST controllers with @RestController.
    • Map HTTP methods (GET, POST, PUT, DELETE) to appropriate controller methods using @GetMapping, @PostMapping, etc.
    • Ensure request and response payloads are well-defined (DTOs) and documented.
    • Controllers should primarily handle HTTP request/response mapping and delegate actual business logic to service layers.
  • DTOs (Data Transfer Objects): Define separate DTOs for request and response bodies to decouple internal domain models from API contracts. Use validation annotations (e.g., @Valid, @NotNull, @Size) on DTOs.

  • Logging (Slf4j & Logback):

    • Use org.slf4j.Logger for all logging within the application.
    • Configure logback-spring.xml for log levels, appenders, and output formats.
    • Log messages should be descriptive and provide sufficient context. Avoid logging sensitive information.
    • Use parameterized logging to improve performance and prevent string concatenation overhead (e.g., logger.debug("Processing user: {}", userId);).
    • Standard logging levels: ERROR, WARN, INFO, DEBUG, TRACE.

3.4. Dependency Management (Gradle)

  • Build File: build.gradle must be well-organised, with clear sections for plugins, dependencies, and tasks.
  • Dependency Versions: Centralise dependency versions in the build.gradle file or gradle.properties where possible to ensure consistency.
  • Plugin Management: Explicitly declare Gradle plugins and their versions.
  • Avoid Unnecessary Dependencies: Only include dependencies that are actively used by the project. Regularly review and clean up unused dependencies.

3.5. Security (Spring Security)

  • Mandatory Use: Spring Security is mandatory for all Spring Boot web applications.
  • Authentication & Authorisation: Configure authentication mechanisms (e.g., OAuth 2.0, JWT, session-based) and authorisation rules within SecurityConfig.java.
  • CSRF Protection: Ensure CSRF protection is enabled for state-changing operations unless a valid reason for disabling it exists (e.g., stateless API where other mechanisms are in place).
  • CORS: Properly configure Cross-Origin Resource Sharing (CORS) based on frontend deployment requirements.
  • Third-Party Identity Providers: When integrating with third-party identity providers (e.g., Microsoft Entra ID), follow best practices for secure token handling and user provisioning. Sensitive credentials must be managed securely (e.g., environment variables, Vault).
  • Input Validation: Always validate all user inputs on the server-side to prevent common vulnerabilities like SQL injection, XSS, etc.
  • Content Security Policy (CSP): Consider implementing a strong CSP for the frontend to mitigate XSS attacks.

3.6. API Design (RESTful)

  • RESTful Principles: Adhere to RESTful principles:
    • Resources: Model data as resources identifiable by URIs.
    • HTTP Methods: Use standard HTTP methods (GET for retrieval, POST for creation, PUT for full update, PATCH for partial update, DELETE for removal) appropriately.
    • Statelessness: APIs should be stateless; each request from a client to a server must contain all the information needed to understand the request.
    • Hypermedia (HATEOAS): While not strictly mandatory for all internal APIs, consider HATEOAS for public-facing APIs for discoverability.
  • URIs:
    • Use plural nouns for collection resources (e.g., /users, /products).
    • Use hyphens for readability in URIs (e.g., /user-accounts).
    • Avoid verbs in URIs (e.g., instead of /getAllUsers, use /users).
  • Status Codes: Use appropriate HTTP status codes to indicate the outcome of an API request (e.g., 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error).
  • Response Formats: JSON is the preferred response format. Ensure consistent JSON structure for both success and error responses.
  • Versioning: Currently, versioning is not implemented. Should it become necessary, the preferred approach will be to use a custom request header, such as X-Endpoint-Version, to indicate the desired API version. This allows for flexible version management without polluting the URI.
    • Example: GET /api/users with header X-Endpoint-Version: 1.0

4. Frontend Standards (React 19, Ant Design, Redux, Axios, pnpm)

4.1. Project Structure

The frontend application follows a standard modern React project structure, leveraging Vite for development. The src directory organises code by concern, promoting maintainability and scalability.

├── eslint.config.js               // ESLint configuration for code quality
├── index.html                     // Main HTML entry point
├── package.json                   // Project metadata and dependencies
├── pnpm-lock.yaml                 // pnpm lock file for deterministic dependencies
├── public                         // Static assets served directly (e.g., global images)
│   └── vite.svg
├── src                            // Main application source code
│   ├── api                        // Low-level API client definitions (e.g., Axios instances, raw request functions)
│   │   ├── auth                   // Authentication-specific API calls
│   │   │   └── index.ts
│   │   └── index.ts               // Centralised API export
│   ├── assets                     // Static assets used within components (e.g., component-specific images, icons)
│   │   ├── dingtalk.svg
│   │   ├── microsoft.svg
│   │   ├── password.svg
│   │   ├── react.svg
│   │   └── wecom.svg
│   ├── components                 // Reusable UI components (display logic, no business logic)
│   │   └── protected-route        // Example: A route guard component
│   │       └── index.tsx
│   ├── config                     // Application-wide configuration values
│   │   ├── index.ts
│   │   └── msal-config            // Microsoft Entra ID (MSAL) specific configuration
│   │       └── index.ts
│   ├── hooks                      // Custom React Hooks for reusable logic
│   ├── index.css                  // Global CSS styles
│   ├── layouts                    // Page layouts (e.g., header, sidebar, footer structures)
│   ├── main.tsx                   // Main React application entry point (root component rendering)
│   ├── page                       // Route-specific components/views (pages)
│   │   ├── login                  // Login page components
│   │   │   ├── index.tsx
│   │   │   ├── msal-login         // MSAL-specific login component
│   │   │   │   └── index.tsx
│   │   │   └── username-and-password // Username/password login component
│   │   │       └── index.tsx
│   │   └── register               // Registration page components
│   │       └── index.tsx
│   ├── service                    // Business logic layer, orchestrating API calls and state management
│   │   ├── auth                   // Authentication-related business logic
│   │   │   ├── index.ts
│   │   │   └── msal               // MSAL-specific authentication logic
│   │   │       └── index.ts
│   │   └── web-client             // Generic web client utilities or shared service logic
│   │       └── index.ts
│   ├── store                      // Redux store and associated slices
│   │   ├── auth-slice             // Redux slice for authentication state management
│   │   │   └── index.ts
│   │   └── index.ts               // Redux store configuration
│   ├── types                      // TypeScript type definitions
│   │   └── index.ts
│   └── vite-env.d.ts              // Vite-specific environment type declarations
├── tsconfig.app.json              // TypeScript configuration for application code
├── tsconfig.json                  // Base TypeScript configuration
├── tsconfig.node.json             // TypeScript configuration for Node.js environments (e.g., Vite config)
└── vite.config.ts                 // Vite build configuration

Key Observations and Specific Directives:

  • src/api vs. src/service: This structure suggests a clear separation:
    • src/api: Should contain raw, low-level functions for making HTTP requests (e.g., Axios calls) without much business logic. Its primary role is to map to backend API endpoints and handle basic request/response formatting for the transport layer.
    • src/service: Should encapsulate higher-level business logic. Components in the service layer will utilise functions from the api layer, combine data from multiple API calls, implement retry logic, and handle data transformation before providing it to page components or Redux store slices.
  • src/assets vs. public:
    • public directory: For static assets that are served directly by the web server at /, without being processed by Vite's build pipeline (e.g., vite.svg). Ideal for small, global assets or robots.txt.
    • src/assets: For images, icons, or other media assets that are imported into React components and handled by Vite's asset bundling.
  • src/components vs. src/page:
    • src/components: Dedicated to small, reusable UI components that are generally "dumb" – they receive data via props and emit events. They should not contain route-specific logic, significant business logic, or directly manage global state (though they can dispatch actions).
    • src/page: Contains "smart" components that represent specific application views or routes. Pages orchestrate components, fetch data (often via service or store), and handle route-specific logic.
  • src/config: Centralises application-wide configurations, including msal-config, which is essential for managing Microsoft Entra ID integration parameters.
  • src/store: Adheres to the Redux "slices" pattern for managing global application state, with auth-slice being a clear example of separating authentication-related state.
  • src/hooks: Explicitly defined for custom React Hooks, promoting the reuse of stateful logic across components.
  • src/layouts: Contains components for structuring overall page layouts, ensuring a consistent application shell across different views.
  • TypeScript Configuration: Multiple tsconfig.json files (tsconfig.json, tsconfig.app.json, tsconfig.node.json) are used to manage TypeScript settings for different compilation contexts (e.g., application code, Vite configuration files). This is a good practice for modern TypeScript projects.

4.2. React & Component Standards

  • Functional Components & Hooks: Prefer functional components with React Hooks over class components for new development.
  • Props:
    • Define interface or type for component props to ensure type safety.
    • Destructure props at the component's entry point for clarity.
  • State Management (Redux):
    • Use Redux Toolkit for efficient and boilerplate-reducing Redux development.
    • Organise Redux logic into "slices" (reducers, actions, and selectors for a specific feature) using createSlice.
    • Follow the "ducks" pattern or "slices" approach for co-locating Redux logic.
  • Component Composition: Break down complex UI into smaller, reusable, and single-responsibility components.
  • Ant Design:
    • Leverage Ant Design components for a consistent UI/UX.
    • Customise Ant Design themes and styles consistently across the application using Less variables or CSS-in-JS solutions if preferred.
  • Accessibility: Design and implement components with web accessibility (a11y) in mind from the outset.

4.3. API Communication (Axios)

  • Axios Instances: Create a centralised Axios instance for API calls to apply common configurations (base URL, headers, interceptors).
  • Interceptors: Use Axios interceptors for:
    • Adding authentication tokens to outgoing requests.
    • Handling global error responses (e.g., showing notifications for 401 Unauthorized).
    • Logging requests/responses in development.
  • Error Handling: Centralise API error handling within Axios interceptors or custom utility functions to provide consistent user feedback.

4.4. Dependency Management (pnpm)

  • Strictness: Utilise pnpm's strict dependency management to ensure a more deterministic node_modules structure and efficient disk space usage.
  • Workspaces: If using a monorepo approach, configure pnpm workspaces for streamlined dependency management across multiple frontend packages.
  • Audit: Regularly audit frontend dependencies for known vulnerabilities using pnpm audit.

5. Documentation & Comments

  • Javadoc (Backend):
    • All public classes, methods, and significant fields in backend Java code must have comprehensive Javadoc comments.
    • Javadoc should explain the purpose, parameters (@param), return values (@return), and exceptions thrown (@throws).
  • Frontend Comments:
    • Use clear and concise comments where the code's intent is not immediately obvious.
    • Components, utility functions, and Redux slices should have introductory comments explaining their purpose.
  • Architecture Documentation: An overarching architecture document for the project is mandatory and currently under development. This document will describe the system's high-level design, key components, data flows, and architectural choices.
  • README.md: Each project (or monorepo root) should have a README.md file providing:
    • Project title and description.
    • Setup and installation instructions.
    • Build and run commands.
    • Testing instructions.
    • Deployment guidelines.
    • Links to relevant documentation (e.g., Architecture Document).

6. Deployment (Docker Support)

  • Dockerfiles: Provide a Dockerfile for the backend application, enabling containerised deployment.
  • Lightweight Images: Aim for lightweight Docker images by using appropriate base images (e.g., eclipse-temurin:17-jdk-focal, node:19-alpine for frontend) and multi-stage builds.
  • Configuration: Ensure environment-specific configurations (e.g., database connection strings, external service URLs) are managed through environment variables injected into the Docker container.
  • Health Checks: Implement Spring Boot Actuator endpoints (e.g., /actuator/health) for readiness and liveness probes in container orchestration environments.
  • Logging: Configure containerised logging to output to stdout and stderr for easy collection by log aggregation systems.

7. Version Control & Code Review (GitFlow)

  • GitFlow Workflow: The GitFlow branching model is to be used for version control, comprising main, develop, feature, release, and hotfix branches.
    • main: Production-ready code. Only release and hotfix branches are merged into main.
    • develop: Integration branch for upcoming features.
    • feature/*: Branches for new features, branched from develop.
    • release/*: Branches for preparing a new production release, branched from develop.
    • hotfix/*: Branches for urgent production bug fixes, branched from main.
  • Pull Requests (PRs): All code changes (except direct pushes to feature branches) must be submitted via Pull Requests.
  • Code Review:
    • Every Pull Request must be reviewed by at least one other developer.
    • Reviewers are responsible for checking adherence to this code standard, code quality, logical correctness, and test coverage.
    • IntelliJ IDEA's integrated code analysis tools should be run locally before creating a PR.
  • Commit Messages: Write clear, concise, and descriptive commit messages that explain what the change is and why it was made. Follow a conventional commit format if possible (e.g., feat: add new user registration endpoint).