
Table of Contents
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.
gradle.properties and configure your CI action (e.g., actions/setup-gradle) to persist the ~/.gradle/caches directory. Use content-addressable keys based on build.gradle.kts hashes to ensure invalidation only when dependencies actually change.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:
./gradlew clean assemble --scan./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.
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
--infologging 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-javawith full version strings (e.g.,21.0.4+7), not just major versions. - Daemon state leakage: Always use
--no-daemonin 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.propertiesis 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.
| Criteria | Gradle Native (setup-gradle@v4) | Manual actions/cache |
|---|---|---|
| Setup complexity | Single step, zero config | Requires path/key tuning |
| Cache granularity | Task-level + dependency | Directory-level only |
| Invalidation accuracy | Content-addressable (automatic) | File-hash based (manual) |
| Remote cache support | Built-in (GE/S3/HTTP) | Not supported |
| Kotlin Multiplatform | Full awareness | Blind to KMP structure |
| Maintenance burden | Low (vendor maintained) | High (self-managed) |
| Best for | All 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.
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.