GitHub Copilot with IntelliJ IDEA
IntelliJ IDEA is where most Java and Kotlin work happens, and it brings something to Copilot that lighter editors do not: a complete, resolved model of your project. Every symbol, every reference, every inheritance relationship, every Gradle or Maven dependency.
This lesson is about using that. The Copilot plugin itself is the same one described in GitHub Copilot with JetBrains IDEs — what changes here is the language ecosystem around it, and IntelliJ’s ability to check the model’s work.
Copilot support
Copilot in IntelliJ IDEA (JetBrains plugin)
One plugin across the whole JetBrains family. Core features are supported; much of the customisation layer is still in preview.
- Code completionSupported
- ChatSupported
- Agent modeSupported
- Custom instructionsPreview
- MCPSupported
- Copilot code reviewSupported
Key takeaways
- Copilot in IntelliJ is the standard JetBrains plugin — everything in the JetBrains lesson applies unchanged.
- IntelliJ’s static analysis is the fastest available review of generated code. Read the gutter before you read the diff.
- Build files are context. An open
build.gradle.ktsorpom.xmlsharply improves dependency and version accuracy. - For Java, type declarations do the work a prompt would otherwise have to do.
- For Kotlin, nullability and coroutine scope are where generated code most often drifts from what the project actually does.
- Use IDE refactorings for mechanical change and Copilot for change that needs judgement.
Setup, briefly
Install the GitHub Copilot plugin from the Marketplace inside IntelliJ, restart, then Tools → GitHub Copilot → Login to GitHub and complete the device-code flow in your browser.
Build files are context
The single highest-value habit in IntelliJ is keeping your build file open when you ask Copilot anything involving dependencies.
build.gradle.kts, build.gradle and pom.xml state which libraries are
available and at what versions. Without one in context, the model falls back on
what is most common in training data — which is how you end up with an answer
using a library you do not have, or an API that changed two major versions ago.
Using only libraries already declared in my build file, write a method that retries this HTTP call with exponential backoff. If nothing suitable is present, say so and tell me what I would need to add rather than assuming it.
That last clause matters. Left unconstrained, the model resolves the gap by inventing an import. Asked explicitly, it will usually tell you.
Java: let the types carry the prompt
Java’s verbosity is an asset here. A fully declared signature narrows the space of plausible completions far more effectively than any English description.
Write this:
public Optional<Customer> findByEmailIgnoringCase(String email) {and completion has been told the lookup may fail, that the input is a string, that comparison is case-insensitive, and — from the surrounding class — where the data comes from. There is very little left to guess.
Compare that with writing a comment describing the same thing above an empty method. The comment is a hint; the signature is a constraint.
The pattern extends further than method signatures:
- Declare the collection type you mean.
List<OrderLine>versusSet<OrderLine>changes duplicate handling, and the model will follow whichever you wrote. - Use your domain types rather than primitives. A method taking
CustomerIdgets better completions than one takingString, because the type names its meaning. - Write the
throwsclause first. It tells the model what failures are expected to escape rather than be handled inline.
Generating JUnit tests
/tests works well in Java, on one condition: give it an existing test class to
imitate. Test conventions vary enormously between projects — JUnit 4 versus 5,
Mockito versus MockK, AssertJ versus Hamcrest versus plain assertions, fixture
builders versus inline construction — and without an example the model picks the
most common combination rather than yours.
Generate tests for this service method, matching the framework, assertion style and fixture approach used in the test class I have open. Cover the boundary cases and the failure paths, not just the happy path. Name each test for the behaviour it verifies.
Then read them for the failure that matters: tests that pass regardless of whether the code is correct. A generated test that asserts a mock was called, when the real question is what value came back, is worse than no test — it adds maintenance cost and false confidence at once.
Kotlin: two places generated code drifts
Kotlin support is good, but two areas need attention because the model has seen a great deal of Kotlin written in styles that are not yours.
Nullability. Kotlin’s type system distinguishes String from String?, and
generated code sometimes reaches for !! to bridge the two. In a project that
has decided nullability is expressed in types, that is a bug in the making. State
the rule in your repository instructions and check for !! when reviewing.
Coroutine scope. GlobalScope appears frequently in tutorials and almost
never in well-structured applications. Generated suspend functions may launch
work in a scope that outlives what you intended. Structured concurrency is a
design decision the model cannot infer from a single file.
Rewrite this to use structured concurrency. The caller owns the scope — do not use GlobalScope and do not create a new scope inside the function. Cancellation must propagate to every child.
Kotlin’s expressiveness cuts the other way too. Because idiomatic Kotlin is
compact, a suggestion can be wrong in a small space — a let where you wanted
also, a scope function that returns the wrong receiver. Density means less to
read and more to read carefully.
IntelliJ checks the work
This is the reason to use Copilot in IntelliJ rather than in a lighter editor. The moment you accept a suggestion, IntelliJ’s analysis runs against it:
- Unresolved references are flagged immediately. Invented method names, the most common failure mode, are caught before the build.
- Type errors surface at the point of the mistake rather than in a compiler message about a different line.
- Nullability warnings catch Kotlin
!!and Java null-safety violations against your annotations. - Inspections flag unused parameters, redundant code, deprecated APIs and framework misuse.
- Structural search and “find usages” let you check whether a generated helper duplicates something that already exists.
The failure this does not catch is the important one: code that compiles cleanly, passes inspections, and does the wrong thing. No tooling substitutes for understanding what you accepted.
Multi-module projects
Large IntelliJ projects are usually multi-module — a Gradle or Maven build with a dozen subprojects and a dependency graph between them. That structure creates a failure mode worth naming.
Copilot can suggest code that references a class in a module the current module does not depend on. It compiles in the model’s head and fails in yours, because the dependency does not exist. IntelliJ flags the unresolved reference immediately, which is another reason the gutter habit pays off.
The deeper problem is architectural rather than mechanical: adding the missing dependency to make the error go away may violate the layering the module structure exists to enforce. When a suggestion needs a new module dependency, treat that as a decision rather than a fix.
Implement this within the current module only. If the solution needs something from another module, tell me which one and why rather than writing an import — adding a module dependency is a decision I need to make deliberately.
Two smaller habits help in multi-module work. Open a file from the module you are
targeting rather than relying on the last file you had focused, since proximity
shapes what completion suggests. And when asking architectural questions, say
which module you are in — “in the api module” changes the answer meaningfully
when the same concept appears in three places.
Refactoring belongs to the IDE
Worth repeating in an IntelliJ context because the temptation is strongest here.
IntelliJ’s refactorings operate on the resolved symbol graph. A rename updates every real reference and nothing that merely shares a name — including in Spring XML, JPA queries and resource bundles where the IDE understands the reference. Copilot, asked to do the same, is performing a very well-informed search and replace across whatever it can see.
Use the refactoring for rename, extract method, extract interface, change signature, move class, inline, and introduce parameter. Use Copilot for the questions that precede them:
This class has accumulated several responsibilities. Name them, say which methods and fields belong to each, and give me an ordered list of IntelliJ refactorings that would perform the split safely. Do not write the resulting files.
You get a plan you can evaluate and an execution path the IDE guarantees.
Spring and framework work
Spring projects have a specific weakness: much of the behaviour lives in
annotations, configuration and conventions rather than in the code Copilot is
looking at. A @Transactional annotation on a class changes what every method
in it does, and a file that does not show it gives no hint.
Three things help.
State the framework version. Spring’s APIs and idioms have moved considerably. “Spring Boot 3.4” in your repository instructions prevents answers written for Boot 2.
Open the configuration. When asking about behaviour that configuration controls — security rules, transaction boundaries, serialisation — have the relevant configuration class or YAML open.
Ask about behaviour, not syntax. The syntax is easy to look up; the interaction between annotations is where time is actually lost.
Given the @Transactional annotation on this class and the fact that this method calls another public method on the same bean, will the second method run in the same transaction? Explain what actually happens at runtime, not what the annotations suggest.
That is a real question with a non-obvious answer, and it is the kind of thing worth asking rather than the boilerplate the IDE would generate anyway.
Repository instructions for a Java or Kotlin project
Custom instructions are in public preview in JetBrains, so keep them factual and checkable. A useful starting shape:
# Copilot instructions
## Stack
- Java 21, Spring Boot 3.4, Gradle (Kotlin DSL).
- Tests: JUnit 5 + AssertJ + Mockito. Never JUnit 4, never Hamcrest.
- Persistence: Spring Data JPA. No raw SQL outside `@Query`.
## Conventions
- Constructor injection only; no field `@Autowired`.
- Entities never cross the service boundary — map to DTOs in `api/dto`.
- Prefer `Optional` over returning null from repository-facing methods.
- Use `record` for immutable data carriers.
## Tests
- One behaviour per test; name for the behaviour.
- Use fixtures in `src/test/java/fixtures` rather than inline construction.Every line is verifiable from the output. Vague guidance such as “write clean code” adds tokens and changes nothing.
A realistic session
An afternoon’s work in IntelliJ, showing where each tool belongs.
You pick up a ticket: an existing report endpoint times out on large accounts. Nobody knows why.
Ask for hypotheses, not a fix. Open the service and the repository it calls, then ask what could cause the timeout at scale. You will get a short list — an N+1 query, an unbounded result set, a missing index, work done in a loop that belongs outside it. That list is worth having because it directs where you look next, and it takes ten seconds.
Verify with the IDE, not the model. IntelliJ can show you every caller of the repository method and every place the entity is touched. Use “find usages” to check which hypothesis is plausible. The model guesses; the symbol graph knows.
Confirm with real data. Turn on SQL logging and run the endpoint. If it is an N+1, you will see it — hundreds of nearly identical queries. This is the step that converts a hypothesis into a fact, and it is not one to delegate.
Now ask for the fix, with the fact in hand.
This repository method triggers one query per order line because the association is lazy and the service iterates it. Rewrite the query to fetch the association in one round trip using the JPA approach already used elsewhere in this repository interface. Keep the method signature unchanged.
Check what came back. A fetch join changes result cardinality; a generated query may return duplicates you then need to handle. This is exactly the class of error that compiles, passes inspections, and is wrong.
Add a test that would have caught it. Not a test that the method returns something — a test that asserts the query count, if your setup supports it, or at minimum one that exercises the large-account case.
The shape here is worth generalising. Copilot was useful twice: generating hypotheses at the start, and writing the fix once the problem was known. The diagnosis in between was IDE work and observation, and delegating it would have produced a confident answer with nothing behind it.
Security
Frequently asked questions
Does Copilot work in IntelliJ IDEA Community Edition? Yes. Community Edition is on GitHub’s compatibility list. You need a Copilot subscription, not IDEA Ultimate.
Is the plugin different from the one for PyCharm or WebStorm? No — it is the same plugin across the JetBrains family. What differs is the language ecosystem you use it in.
Why does it suggest libraries I do not have?
Because your build file was not in context. Open build.gradle.kts or pom.xml
and ask again.
Should I use /tests or write tests myself?
Use it, with an existing test class open so the output matches your conventions —
then read the tests for whether they would actually fail if the code were wrong.
Can Copilot do a rename refactoring for me? It can try. IntelliJ’s rename is correct by construction and Copilot’s is not; use the IDE for anything it has a refactoring for.
Why is generated Kotlin using GlobalScope?
Because a great deal of published Kotlin does. State your concurrency rules in
repository instructions and reject it in review.
Does IntelliJ’s own AI Assistant conflict with Copilot? They coexist, but two sources of inline completion in one editor is confusing. Disable one plugin’s completion and keep the other’s.
Next steps
PyCharm covers the same plugin in a Python context, where interpreters and virtual environments replace build files as the context that matters most. Android Studio covers the JetBrains IDE with the most unusual situation — a first-party AI assistant of its own.
For the platform-wide picture, return to GitHub Copilot with JetBrains IDEs. To see how IntelliJ’s position compares across editors, see the matrix in GitHub Copilot for IDEs.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.
Was this lesson helpful?
Your answer is stored in this browser and is not sent anywhere.