Java and Kotlin in VS Code: Fix Maven Interop and Imports

Fix Java/Kotlin interop issues in VS Code and VSCodium with Maven compiler settings, Kotlin source roots, and Java-facing service contracts.

Java and Kotlin in VS Code: Fix Maven Interop and Imports

In May 2026, JetBrains released the official Kotlin extension for Visual Studio Code in Alpha. That is good news for developers who want to work with Kotlin outside IntelliJ IDEA, but mixed Java/Kotlin projects can still be tricky.

I recently moved a backend project from IntelliJ IDEA to VS Code/VSCodium. Maven compiled the project successfully, but the editor still reported unresolved Java imports, broken static imports, and inconsistent Java/Kotlin navigation.

This post documents the fixes that worked for me: making the Maven configuration explicit, exposing Java-facing service contracts, and avoiding Kotlin-generated boundaries in Java code.

Java and Kotlin in the same codebase

I have a few backend projects that mix Java and Kotlin. I did this in professional projects and I like to have some pet projects that have this kind of complexity. Java is a great language, but Kotlin is moving fast and can cover backend and mobile development. Kotlin Multiplatform is mature enough for serious mobile experimentation, especially when sharing code across iOS and Android.

I still enjoy working with IntelliJ IDEA, but I am trying to move most of my projects to VSCodium. VSCodium has the benefits of VS Code without the MS Telemetry.

In this post, I show the fixes I had to apply when moving a working project from IntelliJ IDEA to VS Code/VSCodium.

I have written about mixed Java/Kotlin projects before:

The Friction Zone: Why mixing Kotlin, Java, Hibernate and Lombok is a technical debt nightmare
Transitioning a legacy Java monolith to Kotlin sounds like a modern developer’s dream, but when that project relies on Hibernate and Lombok, the “100% interoperability” promise quickly unravels. This post explores the deep-rooted architectural conflicts that occur when Kotlin’s opinionated, null-sa…
Migration from Kotlin to Java, notes
Steps for the migration from Kotlin to Java, notes

Common import issues in Java

Mixed Java/Kotlin projects can compile perfectly with Maven and still look broken in VS Code.
That was exactly the problem I hit in this codebase:

- Java classes could not reliably resolve Kotlin services

- Java static imports from Kotlin top-level functions failed with static import only from classes and interfaces

The 3 layers

In a mixed project in VS Code, there are 3 separate systems involved:

  1. Maven decides how the project builds.
  2. VS Code + Red Hat Java decides how the project is imported into the Java language server.
  3. Java/Kotlin interop decides how source files see each other.

The confusing part is that these layers can disagree.

In my case:

  • Maven compiled the backend with Java 25
  • Kotlin compiled fine once the plugin version matched the JVM target
  • and Java files still had trouble resolving Kotlin classes in some places

Fix the Maven side

I made the Java level explicit in Maven:

<properties>
    <java.version>25</java.version>
    <maven.compiler.release>${java.version}</maven.compiler.release>
    <kotlin.version>2.4.0</kotlin.version>
</properties>

And the backend compiler configuration also reflects that:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
</plugin>

The Kotlin Maven plugin configuration:

<plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <source>src/main/java</source>
                                <source>src/main/kotlin</source>
                            </sourceDirs>
                            <compilerPlugins>
                                <plugin>spring</plugin>
                                <plugin>all-open</plugin>
                            </compilerPlugins>
                            <pluginOptions>
                                <option>all-open:annotation=jakarta.persistence.Entity</option>
                                <option>all-open:annotation=jakarta.persistence.MappedSuperclass</option>
                                <option>all-open:annotation=jakarta.persistence.Embeddable</option>
                            </pluginOptions>
                        </configuration>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
                <configuration>
                    <jvmTarget>${jvm.target}</jvmTarget>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
            </plugin>

That is necessary because mixed Java/Kotlin support gets much easier when the build configuration is explicit.

Symptom: Java cannot resolve Kotlin Services

Once the Java version problem was fixed, another class of errors showed up:

import ch.genidea.stockdb.backend.maintenance.MaintenanceService;

and:

import ch.genidea.stockdb.backend.rs.service.DailyRelativeStrengthService;

were reported as unresolved by the IDE.

The services existed, but they were in Kotlin:

  • /backend/src/main/kotlin/ch/genidea/stockdb/backend/maintenance/MaintenanceService.kt
  • /backend/src/main/kotlin/ch/genidea/stockdb/backend/rs/service/RelativeStrengthService.kt

Maven could compile them. VS Code still struggled to resolve some Java-to-Kotlin symbols reliably.

The Practical Rule for interop: use Java for boundaries, Kotlin for implementations

The pattern that worked best was:

  1. Put the public contract in Java
  2. Keep the implementation in Kotlin
  3. Inject by interface from Java

This gives the IDE a Java type to resolve, while letting Kotlin stay in the codebase naturally.

Example: MaintenanceService

I introduced a Java interface:

package ch.genidea.stockdb.backend.maintenance;

public interface MaintenanceService {

    void deactivateTickersWithoutPrices();

    void recalculateRsView();

    void refreshLeaderScoreView();

    void refreshRsLeadersView();
}

File:
/backend/src/main/java/ch/genidea/stockdb/backend/maintenance/MaintenanceService.java

Then I kept the implementation in Kotlin:

package ch.genidea.stockdb.backend.maintenance

@Service
class MaintenanceServiceImpl(
    private val tickerRepository: TickerRepository,
    private val jdbcTemplate: JdbcTemplate
) : MaintenanceService {

    override fun deactivateTickersWithoutPrices() { ... }

    override fun recalculateRsView() { ... }

    override fun refreshLeaderScoreView() { ... }

    override fun refreshRsLeadersView() { ... }
}

File:
/backend/src/main/kotlin/ch/genidea/stockdb/backend/maintenance/MaintenanceService.kt

Now Java code depends on the Java interface:

private final MaintenanceService maintenanceService;

instead of directly depending on a Kotlin class.

Example: DailyRelativeStrengthService

The same pattern was applied here.

Java contract:

package ch.genidea.stockdb.backend.rs.service;

public interface DailyRelativeStrengthService {

    void calculateRelativeStrengthRaw(int startDate);
}

File:
/backend/src/main/java/ch/genidea/stockdb/backend/rs/service/DailyRelativeStrengthService.java

Kotlin implementation:

@Service
class DailyRelativeStrengthServiceImpl(
    private val dailyPriceRepository: DailyPriceRepository,
    private val tickerRepository: TickerRepository,
    private val dailyRelativeStrengthRepository: DailyRelativeStrengthRepository,
    private val statusService: StatusService
) : DailyRelativeStrengthService {

    override fun calculateRelativeStrengthRaw(startDate: Int) {
        ...
    }
}

File:
/backend/src/main/kotlin/ch/genidea/stockdb/backend/rs/service/RelativeStrengthService.kt

And Java code like RoutineService.java now references a Java-facing type:

private final DailyRelativeStrengthService dailyRelativeStrengthService;

Example: RelativeStrengthPositionService

Again, the contract is Java and the implementation stays Kotlin.

Java interface:

package ch.genidea.stockdb.backend.rs.service;

import ch.genidea.stockdb.backend.rs.dto.RSPositionDto;
import java.util.List;

public interface RelativeStrengthPositionService {

    void calculateRSPosition();

    void processSingleDay(int date);

    RSPositionDto getLastForTicker(String ticker);

    List<RSPositionDto> getAllLastMonths(int months);
}

File:
/backend/src/main/java/ch/genidea/stockdb/backend/rs/service/RelativeStrengthPositionService.java

Kotlin implementation:

@Service
class RelativeStrengthPositionServiceImpl(
    private val dailyRelativeStrengthRepository: DailyRelativeStrengthRepository,
    private val dailyPriceRepository: DailyPriceRepository,
    private val rSPositionRepository: RSPositionRepository,
    private val marketSurgeService: MarketSurgeService
) : RelativeStrengthPositionService {
    ...
}

File:
/backend/src/main/kotlin/ch/genidea/stockdb/backend/rs/service/RelativeStrengthPositionService.kt

Avoid Java static imports from Kotlin top-level functions

Another failure mode showed up here:

import static ch.genidea.stockdb.backend.DateUtilityKt.getTodayISO;

VS Code complained:

static import only from classes and interfaces

This is valid at the build level because Kotlin generates a synthetic class named DateUtilityKt, but it is not a great Java-facing boundary for IDE tooling.

The fix was to introduce a Java utility class:

package ch.genidea.stockdb.backend;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public final class DateUtility {

    private DateUtility() {
    }

    public static int convertDateToInteger(LocalDate date) {
        return Integer.parseInt(date.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
    }

    public static int getToday() {
        return convertDateToInteger(LocalDate.now());
    }

    public static String getTodayISO() {
        return LocalDate.now().format(DateTimeFormatter.ISO_DATE);
    }
}

File:
/backend/src/main/java/ch/genidea/stockdb/backend/DateUtility.java

Then Java code can use a normal static import:

import static ch.genidea.stockdb.backend.DateUtility.getTodayISO;

That is much more IDE-friendly.

Another possible fix is @file:JvmName("DateUtility"), but for this project I preferred a small Java utility class because the function was mostly consumed from Java. That made the boundary obvious and removed the generated DateUtilityKt class from Java call sites.

Maven configuration for mixed Java/Kotlin projects

The IDE importer also needs help understanding Kotlin source roots.

I explicitly added Kotlin source directories with build-helper-maven-plugin:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.6.1</version>
    <executions>
        <execution>
            <id>add-kotlin-sources</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/main/kotlin</source>
                </sources>
            </configuration>
        </execution>
        <execution>
            <id>add-kotlin-test-sources</id>
            <phase>generate-test-sources</phase>
            <goals>
                <goal>add-test-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/test/kotlin</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

Without that, the VS Code Java importer may compile through Maven but still fail to treat Kotlin folders as part of the project model.

Depending on your setup, you can either rely on the Kotlin Maven plugin’s <extensions>true</extensions> support, or explicitly add Kotlin source roots with build-helper-maven-plugin. I used the explicit approach because I wanted the VS Code Java importer to see the source roots clearly.

Keep Java version configuration consistent

There was an issue with the version drift between tools that caused huge confusion.

In a mixed project, keep all of these aligned:

  • root Maven java.version
  • module compiler settings
  • maven.compiler.release
  • maven.compiler.source
  • maven.compiler.target
  • Kotlin jvmTarget
  • CI Java version
  • the JDK used by VS Code Java language server

When changing Java/Kotlin boundaries or Java levels, this workflow helps:

  1. Run Java: Clean Java Language Server Workspace
  2. Run Java: Reload Projects
  3. Run Developer: Reload Window

Practical recommendations for Java/Kotlin boundaries

Here is the rule set I would use for a team working with Java + Kotlin in VS Code:

Use Java for:

  • service interfaces consumed by Java
  • utility classes used via Java static imports
  • data types imported widely by Java
  • boundaries between major packages when editor stability matters

Use Kotlin for:

  • implementations behind Java interfaces
  • internal domain logic
  • Kotlin-first flows
  • code not heavily imported by Java callers

Avoid when possible:

  • Java importing Kotlin top-level functions through SomethingKt
  • Java depending directly on Kotlin concrete classes
  • tiny Kotlin utility classes used mostly by Java files

The principle that worked best for interop

The most useful mental model from this debugging session was:

Use Java as the contract language, Kotlin as the implementation language.

That keeps:

  • Kotlin in the application
  • Java happy in VS Code
  • Maven configuration straightforward
  • Spring wiring natural

And most importantly, it reduces the gap between "the project builds" and "the editor understands the project".

Closing thought

This was not proof that "Java and Kotlin do not mix".

It was a project-boundary problem.

Once the Java-facing contracts were made explicit and the Maven/Kotlin source roots were declared clearly, the project became much easier for VS Code to understand.

If your team wants Kotlin in a Java-heavy Spring application, you do not need to convert everything. You just need to choose the boundaries carefully.