Skip to content

Contributing Guide

This guide covers everything you need to know to contribute to CoCache: code style enforcement, testing requirements, pull request workflow, and how to add new cache implementations or modules.

Code Style

CoCache enforces code style through Detekt with auto-correct support. The configuration lives at config/detekt/detekt.yml and is applied to all projects.

mermaid
graph LR
    subgraph CodeStyle["Code Style Enforcement"]
        direction TB
        EDIT["Write Code"] --> DETEKT["./gradlew detekt"]
        DETEKT --> PASS{"Pass?"}
        PASS -->|Yes| COMMIT["Commit"]
        PASS -->|No| FIX["./gradlew detektAutoFix"]
        FIX --> DETEKT
    end
    style CodeStyle fill:#161b22,stroke:#6d5dfc,color:#e6edf3
    style EDIT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DETEKT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PASS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style FIX fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style COMMIT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Detekt Configuration

The project overrides several default Detekt rules to allow pragmatic coding patterns. Key overrides:

CategoryRuleSettingRationaleSource
complexityLongParameterListdisabledCache configuration objects naturally have many parametersdetekt.yml:3
complexityTooManyFunctionsdisabledCache interfaces combine multiple operation typesdetekt.yml:5
complexityNestedBlockDepthdisabledDeep nesting acceptable in cache logicdetekt.yml:4
styleMaxLineLength300Accommodates fluent chains and annotationsdetekt.yml:10
styleReturnCountdisabledMultiple early returns improve readability in cache get/set logicdetekt.yml:12
styleMagicNumberdisabledTTL values and cache sizes are domain-specificdetekt.yml:18
styleUnusedPrivateMemberdisabledSome members used reflectivelydetekt.yml:15
styleWildcardImportallows java.util.*Common Java collection importsdetekt.yml:21-24
namingMemberNameEqualsClassNamedisabledCache interface methods naturally match class namesdetekt.yml:30
namingMatchingDeclarationNamedisabledFlexible naming for test and utility classesdetekt.yml:31
exceptionsSwallowedExceptiondisabledException handling in cache fallback pathsdetekt.yml:34
performanceSpreadOperatordisabledSpread operators used in vararg APIsdetekt.yml:40
formattingNoWildcardImportsjava.util.*,org.assertj.core.api.Assertions.* allowedConsistent import styledetekt.yml:43-44

The detekt-formatting plugin is applied alongside the core Detekt plugin via cocache-dependencies, and autoCorrect = true is enabled at the project level (build.gradle.kts:58).

Checking Style

bash
# Run Detekt analysis
./gradlew detekt

# Run Detekt with automatic formatting fixes
./gradlew detektAutoFix

Detekt runs automatically as part of ./gradlew check. Always run detekt before committing to catch issues early.

Testing Requirements

All contributions must include appropriate tests. CoCache uses JUnit 5 (Jupiter) with mockk for mocking and fluent-assert for assertions.

Test Stack

ToolPurposeVersionSource
JUnit 5 JupiterTest framework and parameterized tests6.0.3libs.versions.toml:9
mockkKotlin-native mocking1.14.9libs.versions.toml:11
fluent-assertFluent Kotlin assertions (wraps AssertJ)0.2.6libs.versions.toml:10

Assertion Style

CoCache mandates the fluent-assert library for all test assertions. Never use AssertJ's assertThat() directly.

kotlin
// CORRECT -- fluent-assert extension
import me.ahoo.test.asserts.assert

cache[key].assert().isEqualTo(value)
result.assert().isNotNull()
count.assert().isOne()

// WRONG -- do not use AssertJ directly
assertThat(cache[key]).isEqualTo(value)  // NOT allowed

The fluent-assert pattern provides null-safe assertions and more idiomatic Kotlin syntax.

TCK (Technology Compatibility Kit) Specs

The cocache-test module provides abstract specification classes that define the expected behavior for all cache implementations. New implementations must extend these specs.

mermaid
classDiagram
    class CacheSpec~K, V~ {
        <<abstract>>
        +createCache() Cache~K, V~
        +createCacheEntry() Pair~K, V~
        +get() void
        +set() void
        +setWithTtl() void
        +setWithTtlAmplitude() void
        +evict() void
        +setMissing() void
        +setMissingTtl() void
    }
    class ClientSideCacheSpec~V~ {
        <<abstract>>
        +createCache() ClientSideCache~V~
        +clear() void
    }
    class DistributedCacheSpec~V~ {
        <<abstract>>
        +createCache() DistributedCache~V~
    }
    class DefaultCoherentCacheSpec~K, V~ {
        <<abstract>>
        +createKeyConverter() KeyConverter~K~
        +createClientSideCache() ClientSideCache~V~
        +createDistributedCache() DistributedCache~V~
        +createCacheEvictedEventBus() CacheEvictedEventBus
        +getFromCacheSource() void
        +onEvicted() void
        +onEvictedWhenLoop() void
        +onEvictedWhenCacheNameNotMatch() void
    }
    class MultipleInstanceSyncSpec~K, V~ {
        <<abstract>>
        +createKeyConverter() KeyConverter~K~
        +createClientSideCache() ClientSideCache~V~
        +createDistributedCache() DistributedCache~V~
        +createCacheEvictedEventBus() CacheEvictedEventBus
        +multipleInstanceSync() void
    }
    class CacheEvictedEventBusSpec {
        <<abstract>>
        +createCacheEvictedEventBus() CacheEvictedEventBus
        +publish() void
        +register() void
    }

    CacheSpec <|-- ClientSideCacheSpec
    CacheSpec <|-- DistributedCacheSpec
    CacheSpec <|-- DefaultCoherentCacheSpec
    DefaultCoherentCacheSpec <|-- MultipleInstanceSyncSpec

    style CacheSpec fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ClientSideCacheSpec fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DistributedCacheSpec fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DefaultCoherentCacheSpec fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style MultipleInstanceSyncSpec fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CacheEvictedEventBusSpec fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
Spec ClassTests ForFactory MethodsSource
CacheSpec<K, V>Basic cache operations (get, set, evict, TTL)createCache(), createCacheEntry()cocache-test/.../CacheSpec.kt
ClientSideCacheSpec<V>L2 client-side cache + clear operationcreateCache() (returns ClientSideCache<V>)cocache-test/.../ClientSideCacheSpec.kt
DistributedCacheSpec<V>L1 distributed cache behaviorcreateCache() (returns DistributedCache<V>)cocache-test/.../DistributedCacheSpec.kt
DefaultCoherentCacheSpec<K, V>Full coherent cache with cache source, evict events, cache stampede preventioncreateKeyConverter(), createClientSideCache(), createDistributedCache(), createCacheEvictedEventBus(), createCacheName()cocache-test/.../DefaultCoherentCacheSpec.kt
MultipleInstanceSyncSpec<K, V>Cross-instance cache coherence via event busSame as DefaultCoherentCacheSpeccocache-test/.../MultipleInstanceSyncSpec.kt
CacheEvictedEventBusSpecEvent bus publish and subscribe behaviorcreateCacheEvictedEventBus()cocache-test/.../CacheEvictedEventBusSpec.kt

How to Add a New Cache Implementation

When adding a new ClientSideCache, DistributedCache, or CacheEvictedEventBus implementation, follow these steps:

mermaid
sequenceDiagram
autonumber
    participant Dev as Developer
    participant Impl as New Implementation
    participant TCK as TCK Spec
    participant Test as Test Class
    participant CI as CI Pipeline

    Dev->>Impl: Create implementation class
    Dev->>Test: Create test class extending TCK spec
    Dev->>Test: Implement factory methods
    Dev->>Test: Add implementation-specific tests
    Dev->>CI: ./gradlew :module:check
    CI->>TCK: Run inherited TCK tests
    CI->>Test: Run custom tests
    CI->>CI: Detekt analysis
    CI-->>Dev: All checks pass

Step 1: Implement the Interface

Create your implementation in the appropriate module. For example, a new ClientSideCache:

kotlin
// cocache-core/src/main/kotlin/me/ahoo/cache/client/MyClientSideCache.kt
class MyClientSideCache<V> : ClientSideCache<V> {
    // implement all interface methods
}

Step 2: Create a Test Class Extending the TCK Spec

kotlin
// cocache-core/src/test/kotlin/me/ahoo/cache/client/MyClientSideCacheTest.kt
class MyClientSideCacheTest : ClientSideCacheSpec<String>() {
    override fun createCache(): ClientSideCache<String> {
        return MyClientSideCache()
    }

    override fun createCacheEntry(): Pair<String, String> {
        return "test-key" to "test-value"
    }

    // Add implementation-specific tests here
}

Step 3: Run Tests

bash
./gradlew :cocache-core:test --tests "me.ahoo.cache.client.MyClientSideCacheTest"

The inherited TCK tests will automatically verify all standard cache behaviors. Add custom tests for implementation-specific features.

How to Add a New Module

To add a completely new module (e.g., a new distributed cache backend):

mermaid
graph TD
    subgraph NewModule["Adding a New Module"]
        direction TB
        CREATE["1. Create module directory<br>and build.gradle.kts"] --> SETTINGS["2. Register in<br>settings.gradle.kts"]
        SETTINGS --> BUILD["3. Add dependencies<br>to build.gradle.kts"]
        BUILD --> IMPL["4. Implement interfaces"]
        IMPL --> TEST["5. Write tests extending TCK specs"]
        TEST --> ROOT["6. Update root build.gradle.kts<br>if needed"]
        ROOT --> VERIFY["7. ./gradlew :new-module:check"]
    end
    style NewModule fill:#161b22,stroke:#6d5dfc,color:#e6edf3
    style CREATE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SETTINGS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style BUILD fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IMPL fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style TEST fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ROOT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style VERIFY fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Step 1: Create Module Directory

cocache-my-backend/
  build.gradle.kts
  src/
    main/kotlin/me/ahoo/cache/mybackend/...
    test/kotlin/me/ahoo/cache/mybackend/...

Step 2: Register in settings.gradle.kts

Add the module to settings.gradle.kts:

kotlin
include(":cocache-my-backend")

Step 3: Configure build.gradle.kts

kotlin
// cocache-my-backend/build.gradle.kts
dependencies {
    api(project(":cocache-core"))
    // or api(project(":cocache-spring")) for Spring integration
    // add backend-specific dependencies
    testImplementation(project(":cocache-test"))
}

The module will automatically inherit:

  • JDK 17 toolchain
  • Kotlin compiler flags (-Xjsr305=strict, -Xjvm-default=all-compatibility)
  • Detekt configuration
  • JUnit 5 test configuration
  • Common test dependencies (mockk, fluent-assert, logback)
  • Maven publishing configuration

Step 4: Write Tests

Extend the appropriate TCK specs from cocache-test and add implementation-specific tests.

Step 5: Update Root Build (if needed)

If the new module should be part of the aggregated coverage report, no changes are needed -- the code-coverage-report automatically includes all libraryProjects.

If the module is an application (like cocache-example), add it to serverProjects in the root build.gradle.kts:

kotlin
// [build.gradle.kts:34-36](https://github.com/Ahoo-Wang/CoCache/blob/main/build.gradle.kts#L34-L36)
val serverProjects = setOf(
    project(":cocache-example"),
    project(":cocache-my-backend"), // add here if it's an application
)

Branch Naming

Follow a consistent branch naming convention:

PatternPurposeExample
feature/<description>New featuresfeature/redisson-distributed-cache
fix/<description>Bug fixesfix/ttl-amplitude-calculation
refactor/<description>Code refactoringrefactor/extract-cache-source
docs/<description>Documentation changesdocs/update-api-reference
chore/<description>Build, CI, dependency updateschore/upgrade-spring-boot

Commit Message Format

Use conventional commit format:

<type>(<scope>): <description>

[optional body]
TypeUsageExample
featNew featurefeat(core): add CaffeineCache implementation
fixBug fixfix(spring-redis): handle null cache values
refactorCode restructuringrefactor(api): extract CacheGetter interface
testAdding or updating teststest(core): add concurrent access spec
docsDocumentationdocs(wiki): add publishing guide
choreBuild, CI, depschore(deps): upgrade Kotlin to 2.3.20
ciCI/CD changesci: add CodeQL analysis workflow

Pull Request Workflow

mermaid
sequenceDiagram
autonumber
    participant Dev as Developer
    participant Branch as Feature Branch
    participant PR as Pull Request
    participant CI as CI Pipeline
    participant Review as Code Review
    participant Main as main

    Dev->>Branch: Create feature branch
    Dev->>Branch: Implement changes
    Dev->>Branch: ./gradlew check
    Dev->>PR: Open Pull Request
    PR->>CI: Trigger integration-test.yml
    CI->>CI: Run all test jobs (parallel)
    CI->>CI: Run codecov.yml
    CI-->>PR: Report status
    PR->>Review: Request review
    Review-->>PR: Approve / Request changes
    Dev->>PR: Address feedback (if any)
    PR->>Main: Merge to main

PR Checklist

Before submitting a pull request, ensure:

  1. All tests pass: ./gradlew check succeeds locally
  2. Detekt clean: ./gradlew detekt passes (or run detektAutoFix first)
  3. TCK specs extended: New implementations extend the appropriate spec classes from cocache-test
  4. Coverage maintained: New code includes test coverage (Codecov target: 60%)
  5. No secrets committed: No credentials, tokens, or environment-specific values in code
  6. API compatibility: Public API changes are backward-compatible or documented as breaking

Released under the Apache License 2.0.