Back to blog
02 July 2026
5 min read

My experience working on a Spring Boot codebase with Copilot

Using Github Copilot on a Spring Boot codebase

Introduction

Where I’m working right now, I was employed as a frontend developer working with Angular. I hadn’t touched Java in a while at work. However, I have worked with it on my own side projects. We had a project that recently needed some Java, and I was able to lend my experience with Spring Boot.

The project involves a microservice that handles the upload of files to S3, and is connected to various other microservices and used by frontend projects. I can’t give any more details than that.

In GitHub Copilot you can choose from a wide range of models. I prefer using Claude Opus 4.6 or 4.7. Those are the models I used while I was working on my ticket.

Overengineering

When you’re in Rome do as the Romans do, that’s how the saying goes right? When I entered this project I didn’t really look around for what already existed. I ended up creating a new class for my new feature’s requirements when there was an existing class that could have handled it.

The lesson learned here is to use AI to actually help you figure out suitable existing places for your new code to go, and avoid re-inventing the wheel. This is perhaps one thing many people miss. Everyone is so trigger-happy and ready to jump to code. It also makes us feel good to feel like we have contributed to the codebase substantially, even when it’s AI doing most of the work.

AI loves to create variable names that aren’t necessary?

I noticed that AI generates unnecessary variable names when it can just access the names from the object. My colleague reviewing my code says that because it’s OOP he prefers to access member variables directly using getters instead of assigning them. Take a look at this code for example:

public Response handleMultiPartUpload() {
	final String bucketName = s3BucketConfig.getName();
	final String objectKey = fileEntity.getS3ObjectKey();
	final String uploadId = someService.initiateThing(
		bucketName, objectKey);
	//...
}

He has a fit here because bucketName and objectKey are only ever used once, so he felt like the variables were not necessary. You can, however, argue that it makes the code more readable, and if you do choose to use the variable elsewhere, then you no longer repeat yourself.

It was also really contradictory because another dev did this:

	public void someFun(final Request abortMultipartRequest) {
		final String bucketName = s3BucketConfig.getVideo();
		final String uploadId = abortMultipartRequest.getUploadId();
		final String objectKey = abortMultipartRequest.getObjectKey();

		final Request abortRequest = AbortMultipartUploadRequest.builder()
			.bucket(bucketName)
			.key(objectKey)
			.uploadId(uploadId)
			.build();
    //...
	}

He would have wanted something like this:

	public void someFun(final Request abortMultipartRequest) {
		final Request abortRequest = AbortMultipartUploadRequest.builder()
			.bucket(s3BucketConfig.getVideo())
			.key(abortMultipartRequest.getObjectKey())
			.uploadId(abortMultipartRequest.getUploadId())
			.build();
    //...
	}

Sure, this is less code, but it’s a little tricky to read; for example, s3BucketConfig.getVideo() gave us bucketName, which was more readable than whatever is being pulled out there. Variable name choices help us make sense of the code.

It’s quite possible that this dev loathes naming things because it’s the 1st hardest thing alongside cache invalidation.

Sonarqube and code quality

Companies should really have some code quality check tools in their CI/CD pipeline, because AI tends to generate average, sloppy code without any docs to guide it, and even if they do exist it might ignore them.

Sonarqube, during my development, found many code smells that would have been disastrous in the future if not picked up. Most importantly, it can also detect insecure coding practices.

So Sonarqube complements developing with AI agents really well.

For 400 Bad Request exceptions just use BadRequestException

I ended up creating a new exception InvalidSomethingException which was a runtime exception. I was advised that this is not necessary and that I should just resort to BadRequestException, which already exists and does what I wanted — just pass it a message.

Every new exception needs a new handler in @ControllerAdvice, which is a multiplication of types for no gain.

Summary

  • Don’t jump straight into code — get AI to help you scout the codebase first so you don’t re-invent the wheel.
  • Watch out for AI spitting out unnecessary intermediate variables; sometimes a direct getter call is cleaner, sometimes a well-named variable wins for readability. Pick your battles.
  • Pair AI with something like Sonarqube in your CI/CD, otherwise you’ll ship average, sloppy code and miss insecure patterns.
  • Don’t create new exception types when BadRequestException already does the job — every new exception drags a new @ControllerAdvice handler with it.