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 3.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

mermaidmermaid flowchart TD CLONE["Clone repo
git clone https://github.com/Ahoo-Wang/CoSky"] --> JDK["Install JDK 17
brew install openjdk@17"] JDK --> REDIS["Start Redis
docker run -d -p 6379:6379 redis"] REDIS --> BUILD["Build project
./gradlew build"] BUILD --> IDE["Open in IntelliJ IDEA
Import as Gradle project"] IDE --> DETEKT["Run Detekt
./gradlew detekt"] DETEKT --> TESTS["Run tests
./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
sites:
- **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.