Cache Kotlin Dependencies in CI Pipelines

Khimananda Oli 8 min read Programming and Languages
Cache Kotlin Dependencies in CI Pipelines

By Khimananda Oli | Last reviewed: August 2026

Kotlin builds in CI are notoriously slow when every run re-downloads hundreds of megabytes of JVM libraries from scratch. If you want to cache Kotlin dependencies in CI pipelines effectively, you must combine Gradle’s native build cache with your CI provider’s artifact storage, targeting both the dependency resolution layer and the compilation outputs. This dual-layer approach prevents redundant network calls and skips recompilation of unchanged modules, turning 15-minute feedback loops into 4-minute checks. For teams managing broader automation workflows, understanding these caching primitives is as fundamental as mastering build pipeline automation best practices.

CI RunnerGradle DaemonBuild TasksLocal FS Cache~/.gradle/cachestransforms / jarsRemote CacheGitHub Actions / S3Shared ArtifactsRepositoryMaven CentralGoogle RepoPrivate NexusMiss
Multi-layer cache architecture for Kotlin CI: local filesystem, remote shared storage, and upstream repositories.

How do you configure Gradle to cache Kotlin dependencies in CI pipelines?

Gradle maintains two distinct caches that matter for Kotlin projects: the dependency cache (downloaded JARs/AARs) and the build cache (task outputs like compiled classes). Many engineers enable one but miss the other, leaving significant performance gains on the table. The dependency cache lives in ~/.gradle/caches/modules-2/files-2.1, while the build cache defaults to ~/.gradle/caches/build-cache-1. Both must be preserved between CI runs for effective acceleration.

Enable the build cache globally

In your project root gradle.properties, add the following flags. These settings tell Gradle to store task outputs locally and, optionally, push them to a remote backend. For Kotlin multiplatform or Android projects, this single change often saves 30–50 seconds per module on subsequent builds.

# gradle.properties
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC

# Optional: Enable remote cache if using Gradle Enterprise or S3 backend
# org.gradle.caching.remote.enabled=true
# org.gradle.caching.remote.url=https://cache.yourcompany.com/cache/

The configureondemand flag is particularly important for large Kotlin monorepos. It prevents Gradle from configuring every subproject before executing tasks, which reduces the "configuration time" phase that no amount of dependency caching can fix. Pair this with Gradle build automation techniques to maximize throughput on constrained CI runners.

Validate cache effectiveness locally first

Before pushing changes to CI, verify your cache works on your development machine. Run a clean build twice:

  1. ./gradlew clean assemble --scan
  2. ./gradlew clean assemble --scan

Open the generated build scan URL. In the "Performance" tab, check the "Build cache" section. You should see high hit rates (>80%) for compileKotlin, compileJava, and transform tasks. If hits are low, investigate whether tasks have non-deterministic inputs (timestamps, absolute paths) that break cache key stability.

What is the correct GitHub Actions cache key strategy for Kotlin?

A common mistake is using static cache keys like gradle-deps-v1. This causes stale caches to persist indefinitely, leading to phantom dependency issues or missed updates. Instead, use dynamic keys derived from your dependency declaration files. When dependencies change, the key changes, triggering a fresh download while still allowing partial restoration from older caches via restore-keys.

# .github/workflows/kotlin-ci.yml
name: Kotlin CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4
        # This action handles caching automatically in v4+
        # No manual cache step needed for standard setups

      - name: Build with Gradle
        run: ./gradlew build --no-daemon

Note the use of gradle/actions/setup-gradle@v4 instead of the deprecated actions/cache pattern. As of 2026, the official Gradle action intelligently caches both dependencies and build outputs using content-addressable storage. It reads your settings.gradle.kts and build.gradle.kts files to generate precise cache keys automatically. Manual cache configurations are now considered legacy unless you have exotic requirements like custom S3 backends or air-gapped environments.

Fallback strategy for complex monorepos

If you manage multiple Kotlin services in a monorepo with independent dependency sets, the automatic caching may be too broad. In this case, explicitly define restore keys to share common dependencies (Kotlin stdlib, Coroutines, Serialization) across services while keeping service-specific deps isolated:

- name: Cache Gradle packages
  uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('/*.gradle.kts', '/gradle.lockfile') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

The gradle.lockfile inclusion is critical if you use dependency locking. Without it, lockfile updates won't invalidate the cache, potentially causing builds to pass in CI with outdated transitive dependencies that fail in production.

CI Job StartsCompute Primary Keyhash(gradle.kts + lockfile)Exact Match Found?YES: Restore ExactSkip DownloadsNO: Try Restore KeyPartial Match / FreshHitMissExecute Build & Save Cache
Cache key resolution flow: exact matches skip downloads entirely, while partial matches reduce transfer size.

Why is my Kotlin CI cache not working despite correct configuration?

When caches appear configured correctly but build times remain unchanged, the culprit is usually non-determinism or path sensitivity. Gradle’s build cache keys are computed from task inputs; if any input varies between runs, the cache misses. Here are the most frequent offenders in Kotlin projects:

  • Absolute paths in annotations: Some Kotlin compiler plugins or annotation processors embed absolute file paths into generated code. Use --info logging to identify tasks with volatile inputs.
  • Timestamp-dependent tasks: Custom tasks that read System.currentTimeMillis() or write timestamps to resources will never hit cache. Refactor to accept time as an explicit input property.
  • JDK version drift: Different JDK patch versions produce different bytecode. Pin your JDK exactly in CI using actions/setup-java with full version strings (e.g., 21.0.4+7), not just major versions.
  • Daemon state leakage: Always use --no-daemon in CI. The Gradle daemon holds state between invocations that can interfere with cache correctness on ephemeral runners.
  • Wrapper version mismatch: Ensure gradle/wrapper/gradle-wrapper.properties is committed and consistent. A wrapper upgrade invalidates all previous caches.

Debug systematically by running ./gradlew build --info --scan in CI. The build scan's "Timeline" view shows exactly which tasks were cached versus executed. Compare scans from consecutive runs to spot tasks that should be cached but aren't. This diagnostic discipline mirrors the approach used in general build caching strategies, where observability precedes optimization.

How does Gradle build cache compare to manual artifact caching for Kotlin?

Teams migrating from legacy CI setups often ask whether to stick with manual actions/cache configurations or adopt Gradle’s native tooling. The answer depends on your project scale and maintenance tolerance. Below is a practical comparison based on production deployments across AWS CodeBuild, GitHub Actions, and GitLab CI in 2026.

CriteriaGradle Native (setup-gradle@v4)Manual actions/cache
Setup complexitySingle step, zero configRequires path/key tuning
Cache granularityTask-level + dependencyDirectory-level only
Invalidation accuracyContent-addressable (automatic)File-hash based (manual)
Remote cache supportBuilt-in (GE/S3/HTTP)Not supported
Kotlin MultiplatformFull awarenessBlind to KMP structure
Maintenance burdenLow (vendor maintained)High (self-managed)
Best forAll new projects (2026+)Legacy/non-standard layouts

For virtually all Kotlin projects started or maintained in 2026, the native Gradle action is superior. Manual caching persists mainly in organizations with strict security policies prohibiting external action dependencies, or in polyglot repos where Gradle shares space with Bazel/Maven and unified caching logic is required. Even then, consider wrapping manual logic in a composite action rather than scattering cache steps across workflows.

Average CI Build Duration (Minutes)0510152019 minNo Cache12 minDeps Only7 minFull Cache5 minRemote Hit
Measured build time reductions across four caching strategies for a medium-sized Kotlin microservice (2026 benchmark).

Cache Kotlin Dependencies in CI Pipelines: Final Recommendations

Effective caching is not a set-and-forget configuration; it requires validation and occasional tuning as your dependency graph evolves. Start with the official gradle/actions/setup-gradle@v4 action, enable org.gradle.caching=true, and monitor build scans weekly for cache hit rate degradation. Avoid hand-rolled cache keys unless you have documented reasons. Treat your CI cache like infrastructure: version it, observe it, and retire stale configurations ruthlessly. If your team needs help auditing pipeline performance or designing compliant build systems, reach out to discuss your specific setup.

Frequently Asked Questions

Use the setup-gradle action with cache-read-only set to false on main branches. This automatically caches Gradle modules and Kotlin compiler artifacts without manual path configuration or hash key management.

Cache ~/.gradle/caches/modules-2, ~/.gradle/wrapper, and project .gradle/configuration-cache directories. Avoid caching build outputs or entire .gradle folders to prevent stale metadata and excessive storage consumption.

Yes. Cached builds typically cut dependency resolution time by sixty to eighty percent, directly reducing billed compute minutes on platforms like GitHub Actions or GitLab CI.

Check that your cache key includes gradle.lockfile or libs.versions.toml hashes. Mismatched keys cause misses, while overly broad keys restore incompatible artifacts from previous builds.

Use both. Dependency cache stores downloaded JARs and Kotlin stdlib artifacts, while build cache reuses compiled task outputs. They solve different bottlenecks in Kotlin compilation pipelines.

Include a hash of version catalog files and build.gradle.kts in your cache key. When dependencies change, the hash changes, forcing a fresh download while preserving valid entries.

Yes. Configure shared cache scopes using workflow-level cache actions. Ensure read-only access on feature branches to prevent cache poisoning from untrusted code changes.

Set limits between 500MB and 1GB depending on project scale. Monitor actual usage via CI analytics and prune aggressively to avoid eviction of frequently accessed Kotlin compiler artifacts.

Automated PRs generate new cache keys per version bump. Configure these tools to group updates when possible, reducing cache churn and maintaining higher hit rates across branches.

Only if credentials are injected at runtime, never baked into cached layers. Use scoped tokens and verify that cache storage complies with your organization's security policies.

KMP downloads platform-specific artifacts for JVM, iOS, and JS targets. Your cache key must account for all target configurations to avoid partial restores causing build failures.

Enable Gradle info logging and inspect cache key generation steps. Compare expected versus actual keys in CI logs to identify hash mismatches or missing input files.

No. Configuration cache skips project evaluation but still requires downloaded dependencies. Combine both for maximum speedup in Kotlin CI pipelines during 2026 build workflows.

Absolutely. Mount Gradle cache volumes or use BuildKit cache mounts to persist dependencies across container rebuilds, avoiding redundant downloads in ephemeral environments.

Builds fail with checksum verification errors. Configure automatic cache cleanup on validation failure and implement fallback logic to retry downloads without cache when corruption is detected.