Skip to content

Contributor Onboarding Guide ​

Welcome to CoSky. This guide will take you from zero to productive contributor. You are expected to know Kotlin or Java, be comfortable with Spring Boot, and have a basic understanding of Redis. Everything else you will learn here.

Part I — Foundations ​

What is CoSky? ​

CoSky is a high-performance microservice governance platform that provides service discovery and configuration management, backed entirely by Redis. Instead of deploying a separate coordination server (like etcd, Consul, or ZooKeeper), CoSky turns your existing Redis infrastructure into a service mesh control plane.

The name comes from Co (configuration) + Sky (service discovery). The project is open-source under Apache 2.0.

Tech Stack ​

LayerTechnology
LanguageKotlin (JVM 17 toolchain)
FrameworkSpring Boot 4.x, Spring Cloud
ReactiveProject Reactor (Mono/Flux)
StorageRedis (via ReactiveStringRedisTemplate)
AtomicityLua scripts for all mutations
BuildGradle (Kotlin DSL)
TestingJUnit 5, MockK, FluentAssert
Code StyleDetekt
BenchmarksJMH (Java Microbenchmark Harness)

Building the Project ​

bash
# Clone the repository
git clone https://github.com/Ahoo-Wang/CoSky.git
cd CoSky

# Build everything (compile + test)
./gradlew build

# Build without tests (faster for development)
./gradlew build -x test

# Check code style
./gradlew detekt

# Run specific module tests
./gradlew :cosky-config:test
./gradlew :cosky-discovery:test

# Run the REST API server locally
./gradlew :cosky-rest-api:bootRun

# Run benchmarks
./gradlew :cosky-config:jmh
./gradlew :cosky-discovery:jmh

Redis Requirement ​

Integration tests require a running Redis instance. The test base class AbstractReactiveRedisTest connects to localhost:6379 by default. Start Redis before running tests:

bash
# Docker
docker run -d -p 6379:6379 redis:latest

# Or via Homebrew
brew services start redis

Part II — Architecture and Domain ​

Module Structure ​

CoSky is organized as a multi-module Gradle project. Understanding the dependency graph is essential before making changes.

mermaid
flowchart TB
    subgraph "Core Layer"
        CORE["cosky-core<br>Namespace, Redis keys,<br>PubSub base"]
    end

    subgraph "Domain Layer"
        CONFIG["cosky-config<br>Configuration CRUD,<br>versioning, rollback"]
        DISCOVERY["cosky-discovery<br>Service registry,<br>discovery, load balancing"]
    end

    subgraph "Spring Cloud Layer"
        SCCORE["cosky-spring-cloud-core<br>Auto-configuration,<br>shared properties"]
        SCCONFIG["cosky-spring-cloud-starter-config<br>Property source locator,<br>config refresh"]
        SCDISC["cosky-spring-cloud-starter-discovery<br>Service registration,<br>discovery client"]
    end

    subgraph "Server Layer"
        RESTAPI["cosky-rest-api<br>REST controllers,<br>dashboard, RBAC"]
    end

    subgraph "Support"
        BOM["cosky-bom"]
        DEPS["cosky-dependencies"]
        TEST["cosky-test"]
    end

    CONFIG --> CORE
    DISCOVERY --> CORE
    SCCORE --> CORE
    SCCONFIG --> CONFIG
    SCCONFIG --> SCCORE
    SCDISC --> DISCOVERY
    SCDISC --> SCCORE
    RESTAPI --> CONFIG
    RESTAPI --> DISCOVERY

    style CORE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CONFIG fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DISCOVERY fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SCCORE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SCCONFIG fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SCDISC fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RESTAPI fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style BOM fill:#2d333b,stroke:#30363d,color:#8b949e
    style DEPS fill:#2d333b,stroke:#30363d,color:#8b949e
    style TEST fill:#2d333b,stroke:#30363d,color:#8b949e
ModulePurposeKey Interfaces
cosky-coreNamespace management, Redis key utilities, PubSub event baseNamespaceService, EventListenerContainer, Namespaced
cosky-configConfiguration CRUD, versioning, rollback, consistency cachingConfigService, ConfigRollback
cosky-discoveryService registry, discovery, load balancing, topologyServiceRegistry, ServiceDiscovery, LoadBalancer
cosky-spring-cloud-coreShared Spring Boot auto-configurationCoSkyProperties, CoSkyAutoConfiguration
cosky-spring-cloud-starter-configSpring Cloud config loading and refreshCoSkyPropertySourceLocator, CoSkyConfigRefresher
cosky-spring-cloud-starter-discoverySpring Cloud service registration and discoveryCoSkyDiscoveryClient, CoSkyAutoServiceRegistration
cosky-rest-apiREST API server, dashboard, security, RBACServiceController, ConfigController
cosky-bomBill of Materials for dependency management—
cosky-testShared test utilities, Lua cleanup scriptsAbstractReactiveRedisTest

Critical rule: cosky-core has zero dependencies on other CoSky modules. The dependency graph must remain acyclic.

The Consistency Pattern: Local Cache + PubSub Invalidation ​

This is the most important architectural concept in CoSky. Read this carefully.

CoSky achieves extreme read performance by combining:

  1. Local in-process cache — data is stored in ConcurrentHashMap inside each application process. Reads hit this cache at nanosecond speed.
  2. Redis PubSub — when data changes in Redis, a PubSub message notifies all subscribers to invalidate or refresh their local cache.
  3. Lazy subscription — the cache entry is created on first access and automatically subscribes to PubSub events for that key.
mermaid
sequenceDiagram
    autonumber
    participant App1 as Application 1
    participant Redis as Redis
    participant App2 as Application 2

    Note over App1,App2: Initial read (cache miss)
    App1->>Redis: getConfig("database.yaml")
    Redis-->>App1: config data
    App1->>Redis: SUBSCRIBE config changes

    Note over App1,App2: Second application reads
    App2->>Redis: getConfig("database.yaml")
    Redis-->>App2: config data
    App2->>Redis: SUBSCRIBE config changes

    Note over App1,App2: Config update
    App1->>Redis: setConfig("database.yaml", newData)
    Redis->>Redis: Lua script: write + PUBLISH
    Redis-->>App1: PubSub notification
    Redis-->>App2: PubSub notification

    Note over App1,App2: Cache refresh
    App1->>Redis: getConfig("database.yaml")
    App2->>Redis: getConfig("database.yaml")
    App1->>App1: Update local cache
    App2->>App2: Update local cache

The consistency wrappers are:

The performance gain is dramatic:

OperationDirect RedisWith Consistency Layer
getConfig~241K ops/s~257M ops/s (1000x faster)
getInstances~227K ops/s~77M ops/s (340x faster)
getServices~305K ops/s~456M ops/s (1500x faster)

Redis Key Schema ​

All Redis keys are namespace-scoped. The namespace acts as a tenant delimiter and is wrapped in Redis hash tags {...} for cluster-mode compatibility.

Configuration keys (ConfigKeyGenerator):

PurposeKey PatternRedis Type
Config index{namespace}:cfg_idxSET
Current config{namespace}:cfg:{configId}HASH
History index{namespace}:cfg_htr_idx:{configId}ZSET
History version{namespace}:cfg_htr:{configId}:{version}HASH

Discovery keys (DiscoveryKeyGenerator):

PurposeKey PatternRedis Type
Service index{namespace}:svc_idxSET
Service stats{namespace}:svc_statHASH
Instance index{namespace}:svc_itc_idx:{serviceId}SET
Instance data{namespace}:svc_itc:{instanceId}HASH

Why hash tags? Redis Cluster routes keys to slots based on the text between { and }. Wrapping the namespace ensures all keys for a namespace land on the same shard, enabling atomic cross-key operations via Lua scripts. See cosky-core/src/main/kotlin/me/ahoo/cosky/core/util/RedisKeys.kt:24.

Lua Scripts for Atomic Operations ​

All write operations execute as Lua scripts inside Redis. This guarantees atomicity — no client can observe a partially-completed state. There are no multi-command transactions in the codebase; everything is a single Lua script call.

Key Lua scripts:

ScriptModulePurpose
config_set.luacosky-configAtomic set + version + history + publish
config_remove.luacosky-configAtomic remove + history + publish
config_rollback.luacosky-configAtomic rollback to target version + publish
registry_register.luacosky-discoveryAtomic register instance + set TTL + publish
registry_deregister.luacosky-discoveryAtomic deregister + delete key + publish
registry_renew.luacosky-discoveryHeart beat renewal with throttled publish
discovery_get_instances.luacosky-discoveryRead instances + lazy expire dead ones

Each Lua script follows the same pattern:

  1. Read current state
  2. Check preconditions (e.g., hash comparison to skip no-op writes)
  3. Perform mutations
  4. PUBLISH a notification to the key's channel
  5. Return a status code

The throttled publish in registry_renew.lua is particularly interesting — it only publishes a renew event when the previous TTL publication is about to expire, reducing PubSub traffic by an order of magnitude. See cosky-discovery/src/main/resources/registry_renew.lua:8.

The Event Model ​

CoSky uses Redis PubSub to propagate state changes. Events are string-based op codes published to Redis channels that map directly to key names.

Config events (ConfigChangedEvent):

EventTriggerOp Code
SETConfig created or updated"set"
REMOVEConfig deleted"remove"
ROLLBACKConfig rolled back to previous version"rollback"

Instance events (InstanceChangedEvent):

EventTriggerOp Code
REGISTERNew instance registered"register"
DEREGISTERInstance explicitly removed"deregister"
EXPIREDInstance TTL expired (lazy cleanup)"expired"
RENEWInstance heartbeat renewed TTL"renew"
SET_METADATAInstance metadata updated"set_metadata"

Part III — Contributing ​

Development Environment Setup ​

mermaid
flowchart TD
    CLONE["Clone repo<br>git clone https://github.com/Ahoo-Wang/CoSky"] --> JDK["Install JDK 17<br>brew install openjdk@17"]
    JDK --> REDIS["Start Redis<br>docker run -d -p 6379:6379 redis"]
    REDIS --> BUILD["Build project<br>./gradlew build"]
    BUILD --> IDE["Open in IntelliJ IDEA<br>Import as Gradle project"]
    IDE --> DETEKT["Run Detekt<br>./gradlew detekt"]
    DETEKT --> TESTS["Run tests<br>./gradlew test"]
    TESTS --> READY["Ready to contribute!"]

    style CLONE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style JDK fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style REDIS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style BUILD fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IDE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DETEKT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style TESTS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style READY fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Prerequisites:

  • JDK 17 — required by the JVM toolchain configuration in build.gradle.kts
  • Redis — required for integration tests
  • IntelliJ IDEA — recommended (Kotlin support is first-class)

Running Tests ​

bash
# Run all tests across all modules
./gradlew test

# Run tests for a specific module
./gradlew :cosky-core:test
./gradlew :cosky-config:test
./gradlew :cosky-discovery:test

# Run a single test class
./gradlew :cosky-config:test --tests "me.ahoo.cosky.config.redis.RedisConfigServiceTest"

# Run tests with verbose output
./gradlew :cosky-config:test --info

All test classes use JUnit 5 and the me.ahoo.test:fluent-assert-core library for assertions. When writing tests, always use:

kotlin
import me.ahoo.test.asserts.assert

// Correct:
actual.assert().isEqualTo(expected)

// Do NOT use AssertJ's assertThat() — it is verbose and not null-safe in Kotlin

Code Style ​

CoSky uses Detekt for static analysis with auto-correct enabled:

bash
# Check for style violations
./gradlew detekt

# Detekt auto-correct is enabled by default in build.gradle.kts
# It will fix formatting issues automatically during build

Key style rules:

  • All source files must have an Apache 2.0 license header
  • Compiler flags: -Xjsr305=strict, -Xjvm-default=all-compatibility
  • All core APIs return Mono or Flux (Project Reactor)
  • All Redis mutations go through Lua scripts — never use multi-command sequences

PR Process ​

  1. Fork the repository (or create a branch if you have push access)
  2. Create a feature branch from main: git checkout -b feature/my-feature
  3. Write tests first — all new features and bug fixes must have tests
  4. Run the full build: ./gradlew build detekt
  5. Commit with a descriptive message
  6. Open a Pull Request against main
  7. CI will run integration tests, benchmarks, code coverage, and Detekt checks

What to Contribute ​

Good first issues:

  • New load balancer strategies — implement the LoadBalancer interface
  • Dashboard improvements — the dashboard/ directory contains a React 19 frontend
  • Documentation — the wiki/ directory contains the VitePress documentation site
  • Test coverage — look for uncovered code paths in the Jacoco reports

Areas requiring careful review (discuss before changing):

  • Lua scripts — they enforce critical consistency invariants
  • Redis key schema — changes break backward compatibility
  • Consistency layer wrappers — they are the performance foundation

Key Files Reference ​

The table below lists the most important source files. Bookmark these — you will reference them frequently.

FilePurpose
cosky-core/.../CoSky.ktBrand constants and key separator
cosky-core/.../Namespaced.ktNamespace defaults and system namespace
cosky-core/.../RedisKeys.ktHash tag wrapping for Redis Cluster
cosky-config/.../ConfigKeyGenerator.ktConfig Redis key patterns
cosky-config/.../ConfigService.ktConfig service interface
cosky-config/.../RedisConfigService.ktConfig Redis implementation
cosky-config/.../RedisConsistencyConfigService.ktConfig consistency wrapper
cosky-config/src/main/resources/config_set.luaAtomic config set + version + history
cosky-discovery/.../DiscoveryKeyGenerator.ktDiscovery Redis key patterns
cosky-discovery/.../ServiceRegistry.ktRegistry interface
cosky-discovery/.../ServiceDiscovery.ktDiscovery interface
cosky-discovery/.../ConsistencyRedisServiceDiscovery.ktDiscovery consistency wrapper
cosky-discovery/.../RedisServiceRegistry.ktRegistry Redis implementation
cosky-discovery/src/main/resources/registry_register.luaAtomic instance registration
cosky-discovery/src/main/resources/registry_renew.luaHeartbeat renewal with throttled PubSub
cosky-rest-api/.../ServiceController.ktService REST endpoints
build.gradle.ktsRoot build configuration
settings.gradle.ktsModule declarations

Contributor Workflow Diagram ​

mermaid
flowchart LR
    subgraph "Local Development"
        BRANCH["Create branch"] --> CODE["Write code + tests"]
        CODE --> DETEKT_LOCAL["Run detekt"]
        DETEKT_local --> BUILD_LOCAL["Run build"]
        BUILD_LOCAL --> COMMIT["Commit"]
    end

    subgraph "Pull Request"
        COMMIT --> PUSH["Push to GitHub"]
        PUSH --> PR["Open PR"]
    end

    subgraph "CI Pipeline"
        PR --> CI_TEST["Integration tests"]
        CI_TEST --> CI_DETEKT["Detekt check"]
        CI_DETEKT --> CI_COVERAGE["Code coverage"]
        CI_COVERAGE --> CI_BENCH["JMH benchmarks"]
    end

    CI_BENCH --> REVIEW["Code review"]
    REVIEW --> MERGE["Merge to main"]

    style BRANCH fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CODE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DETEKT_local fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style BUILD_LOCAL fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style COMMIT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PUSH fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PR fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CI_TEST fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CI_DETEKT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CI_COVERAGE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CI_BENCH fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style REVIEW fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style MERGE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Released under the Apache License 2.0.