Kotlin's modern features improve on Java, but legacy Kotlin projects develop their own forms of debt. VibeRails reads your entire codebase and finds null safety violations, coroutine misuse, and Java interop issues that linters overlook.
Kotlin was designed to fix Java's most painful problems: null pointer exceptions, verbosity, and lack of modern language features. But adopting Kotlin does not eliminate technical debt. It shifts the patterns that debt takes. Legacy Kotlin projects develop their own characteristic problems that are distinct from Java's but equally consequential.
Null safety is Kotlin's headline feature, but it is routinely undermined in practice. The
!! operator (the non-null assertion) is scattered through codebases as a quick
way to satisfy the compiler when a developer is confident a value will not be null. Each
!! is a potential NullPointerException waiting to happen. Platform
types from Java interop make the problem worse: when Kotlin calls Java code, the return types
are treated as having unknown nullability. Without explicit null checks at the boundary,
nulls flow silently from Java into Kotlin code that assumes everything is non-null.
Coroutine misuse is a growing source of bugs in Kotlin projects. Structured concurrency with
coroutines is powerful but has a steep learning curve. Common mistakes include launching
coroutines in GlobalScope instead of a lifecycle-aware scope, failing to handle
cancellation properly, mixing blocking and suspending code in the same context, and catching
CancellationException in a way that breaks cooperative cancellation. These
issues often manifest as memory leaks, dropped operations, or hard-to-reproduce race conditions.
Android projects add lifecycle complexity on top of coroutine concerns. Activities and Fragments
have their own lifecycle states, and operations that outlive the UI scope – network calls,
database writes, animations – must be coordinated with those states. Legacy Android Kotlin
projects often have a mix of approaches: some screens use viewModelScope, others
use lifecycleScope, and some still use older patterns like AsyncTask wrappers or
RxJava chains. The inconsistency makes the codebase harder to reason about and maintain.
Kotlin has capable static analysis tools. Detekt provides configurable rules for code smells, complexity, and style. Android Lint catches platform-specific issues. The Kotlin compiler itself enforces null safety at the type level. But these tools have fundamental limits when it comes to cross-cutting concerns in legacy codebases.
Consider a Spring Boot application written in Kotlin. A service class injects a repository
and calls a method that returns a Java Optional. The Kotlin code unwraps the
Optional using .get() without checking isPresent, trusting that
the database query will always return a result. Detekt will not flag this because the code
is syntactically correct. The Kotlin compiler will not flag it because Optional.get()
returns a platform type. The bug only manifests when the database state changes and the
query returns empty.
Or consider an Android project where a ViewModel launches a coroutine to fetch data from a
remote API. The coroutine updates a MutableStateFlow, which a Composable observes.
But the coroutine does not handle the case where the ViewModel is cleared before the network
call completes. On a fast connection, this never causes a problem. On a slow connection with
screen rotation, the coroutine attempts to emit to a flow that no one is collecting, and the
result is silently lost. A linter can check that a coroutine uses the correct scope, but it
cannot assess whether the entire data flow handles lifecycle transitions correctly.
These cross-module, cross-layer issues require understanding how the components of the application interact, which is beyond what pattern-matching tools can provide.
VibeRails performs a full-codebase scan using frontier large language models. Every Kotlin source file is analysed along with Gradle configuration, Android manifests, resource files, and test suites. The AI reads each file and reasons about its purpose, structure, and relationship to the rest of the project.
For Kotlin code specifically, the review covers:
!! assertions in non-trivial code paths, unchecked platform types from Java interop, nullable types that propagate through chains of function calls without being narrowedGlobalScope usage, missing cancellation handling, blocking calls inside coroutine contexts, exception propagation in structured concurrency hierarchieslet, run, apply), data class misuse for mutable stateEach finding includes the file path, line range, severity level, category, and a detailed description with suggested remediation. Findings are organised into 17 categories so teams can address issues systematically.
Kotlin's interop with Java and its flexible syntax create situations where the right approach
is context-dependent. Is that !! acceptable because the value is guaranteed
non-null by a preceding check in another function? Is the GlobalScope launch
intentional for a fire-and-forget operation, or should it be tied to a lifecycle?
VibeRails supports running reviews with two different AI backends – Claude Code and Codex CLI – in sequence. The first pass discovers issues, the second pass verifies them using a different model architecture. When both models independently flag the same null safety violation or coroutine misuse, confidence is high. When only one model flags something, it warrants closer attention during human triage.
This cross-validation is particularly valuable for Kotlin because the language bridges the Java ecosystem with modern language features, creating a wide spectrum of valid approaches. What looks like a code smell in isolation might be a reasonable compromise given the Java libraries being consumed. Dual-model verification helps distinguish genuine issues from acceptable trade-offs before the team invests time in review.
After triaging findings, VibeRails can dispatch AI agents to implement fixes directly in your
local repository. For Kotlin projects, this typically means replacing !! with
safe calls and proper null handling, scoping coroutines to appropriate lifecycle owners,
adding nullability annotations at Java interop boundaries, restructuring deeply nested scope
functions, or migrating deprecated Android APIs to current alternatives.
Each fix is generated as a local code change you can inspect, test, and commit or discard. The AI works within the conventions of the existing codebase, matching the project's dependency injection style, coroutine patterns, and module structure.
VibeRails runs as a desktop app with a BYOK model – it orchestrates Claude Code or Codex CLI installations you already have. No code is uploaded to VibeRails servers. AI analysis is sent directly to the provider you configured, billed to your existing subscription. The lifetime license is $299 per developer for the lifetime option (or $19/mo monthly). The free tier includes 5 issues per session to evaluate the workflow.
Vertel over je team en doelen. We reageren met een concreet uitrolplan.