In this final installment of our Unlocking AI Assistants series, we will create a Spring Boot application from scratch using an AI coding assistant. The goal is not to just merely create a working application, but to create production-grade code. Enjoy!
Introduction
Some tasks are executed with the help of an AI coding assistant. The responses are evaluated and different techniques are applied, which can be used to improve the responses when necessary.
Let’s try to generate a Spring Boot application by means of using an AI coding assistant. However, some preconditions apply to this Spring Boot application. The most important precondition is that the generated code must be production-grade. Just generating working software is not enough.
The tasks are executed with the IntelliJ IDEA AI coding assistant.
The setup used in this blog is as inference engine and qwen2.5-coder:7b as model. This runs on GPU.
The sources used in this blog are available at while the explanation of how the project is created can be found .
Before we proceed, if you’d like to review the first three parts of this series, you can click on the links below:
Prerequisites
Prerequisites for reading this blog are:
Create a Skeleton
First, some skeleton need to be created because the Spring Boot application must meet some requirements.
- The Rest API must be defined by means of an OpenAPI specification.
- The controller interface must be generated by means of the
openapi-generator-maven-plugin
. - PostgreSQL must be used as database.
- Liquibase must be used to create the database tables.
- jOOQ must be used to access the database;
- The jOOQ classes must be generated by means of the
testcontainers-jooq-codegen-maven-plugin
.
Navigate to and add the following dependencies:
- Spring Web
- PostgreSQL Driver
- JOOQ Access Layer
- Validation
- Liquibase Migration
The following changes are applied to the generated Spring Boot application:
- The controller interface must be generated based on the OpenAPI specification, add plugin
openapi-generator-maven-plugin
and dependencyswagger-annotations
. - Add scope runtime to dependency
liquibase-core
. - Add a file
db.changelog-root.xml
tosrc/main/resources/db/changelog/db.changelog-root.xml
as root file for the Liquibase migration scripts. - The jOOQ classes should be generated, add plugin
testcontainers-jooq-codegen-maven-plugin
. - Remove the test from the test sources.
The changes are applied to branch .
Run the build.
The build fails because the OpenAPI specification is missing. However, this is the starting point.
Generate OpenAPI Specification
The build fails on the OpenAPI specification, which is missing. So, let’s fix this.
Prompt
Enter the prompt.
Generate an OpenAPI specification version 3.1.1.
The spec should contain CRUD methods for customers.
The customers have a first name and a last name.
Use the Zalando restful api guidelines.
Response
The response can be viewed .
Apply Response
Add the response to file src/main/resources/static/customers.yaml
.
Additionally, change the following:
- Change the identifiers from strings to integers.
- Add the identifier to the Customer schema. This way, the identifier will be returned in the responses.
components:
schemas:
Customer:
type: object
properties:
id:
type: integer
format: int64
firstName:
type: string
lastName:
type: string
Run the build. The build is successful and in the directory, target/generated-sources/openapi
, the generated sources are available.
The build shows some warnings, but these can be fixed by adding an operationId
to the OpenAPI specification. For now, the warnings are ignored.
[WARNING] Empty operationId found for path: GET /customers. Renamed to auto-generated operationId: customersGET
[WARNING] Empty operationId found for path: POST /customers. Renamed to auto-generated operationId: customersPOST
[WARNING] Empty operationId found for path: GET /customers/{id}. Renamed to auto-generated operationId: customersIdGET
[WARNING] Empty operationId found for path: PUT /customers/{id}. Renamed to auto-generated operationId: customersIdPUT
[WARNING] Empty operationId found for path: DELETE /customers/{id}. Renamed to auto-generated operationId: customersIdDELETE
The changes can be viewed .
Generate Liquibase Scripts
In this section, the Liquibase scripts will be generated.
Prompt
Open the OpenAPI spec and enter the prompt.
Based on this openapi spec, generate liquibase migration scripts in XML format
Response
The response can be viewed .
Apply Response
The generated XML Liquibase script is entirely correct. Create in directory src/main/resources/db/changelog/migration
a file db.changelog-1.xml
and copy the response into it. Besides that, change the author to mydeveloperplanet
.
Run the build.
The build log shows that the tables are generated.
Feb 23, 2025 12:42:59 PM liquibase.changelog
INFO: Reading resource: src/main/resources/db/changelog/migration/db.changelog-1.xml
Feb 23, 2025 12:42:59 PM liquibase.changelog
INFO: Creating database history table with name: public.databasechangelog
Feb 23, 2025 12:42:59 PM liquibase.changelog
INFO: Reading from public.databasechangelog
Feb 23, 2025 12:42:59 PM liquibase.command
INFO: Using deploymentId: 0310979670
Feb 23, 2025 12:42:59 PM liquibase.changelog
INFO: Reading from public.databasechangelog
Running Changeset: src/main/resources/db/changelog/migration/db.changelog-1.xml::1::yourname
Feb 23, 2025 12:42:59 PM liquibase.changelog
INFO: Table customers created
Feb 23, 2025 12:42:59 PM liquibase.changelog
In directory, target/generated-sources/jooq
, you can also find the generated jOOQ files which are generated.
The changes can be viewed .
Generate Domain Model
In this section, the domain model will be generated.
Prompt
Open the Liquibase migration script and enter the prompt.
Create a domain model based on this liquibase migration script
Response
The response can be viewed .
Apply Response
Create class Customer
in package com.mydeveloperplanet.myaicodeprojectplanet.model
. This clashes with the OpenAPI domain model package.
Change the following lines in the pom.xml:
com.mydeveloperplanet.myaicodeprojectplanet
com.mydeveloperplanet.myaicodeprojectplanet.api
com.mydeveloperplanet.myaicodeprojectplanet.model
Into:
com.mydeveloperplanet.myaicodeprojectplanet.openapi
com.mydeveloperplanet.myaicodeprojectplanet.openapi.api
com.mydeveloperplanet.myaicodeprojectplanet.openapi.model
Run the build, the build is successful.
The changes can be viewed .
Generate Repository
In this section, the repository will be generated.
Prompt
Add the full project and also add the generated jOOQ classes from the directory target/generated-sources/jooq
to the Prompt Context. Note: It seems DevoxxGenie did not add these files at all because they were ignored in the .gitignore
file, see .
Generate a CustomerRepository in order that the operations defined in the openapi spec customers.yaml are supported
Response
The response can be viewed .
The response uses Spring Data JPA and this is not what is wanted.
Prompt
Give explicit instructions to only use dependencies in the pom.xml.
Generate a CustomerRepository in order that the operations defined in the openapi spec customers.yaml are supported.
Only use dependencies available in the pom.xml
Response
The response can be viewed .
The same response is returned. The instructions are ignored.
Prompt
Let’s be more specific. Enter the prompt.
You do not use the dependencies defined in the pom.
You should use jooq instead of jpa
Response
The response can be viewed .
Apply Response
This response looks great. Even an example of how to use it in a service is added.
Create a package com/mydeveloperplanet/myaicodeprojectplanet/repository
and paste the CustomerRepository
code. Some issues are present:
- A
RuntimeException
is thrown when a Customer cannot be found. Probably this needs to be changed, but for the moment this will do. - The package
com.mydeveloperplanet.myaicodeprojectplanet.jooq
could not be found. A Maven Sync solved this issue. Customers.CUSTOMER
could not be found. The following line needed to be addedimport com.mydeveloperplanet.myaicodeprojectplanet.jooq.tables.Customers;
- Still, two compile errors remain due to a nonexistent
exists()
method.
if (dslContext.selectFrom(Customers.CUSTOMERS)
.where(Customers.CUSTOMERS.ID.eq(id))
.exists())
Prompt
Open a new chat window. Open the CustomerRepository
file and enter the prompt.
the .exists() method does not seem to be available, fix the code
Response
The response can be viewed .
Apply Response
The response suggests to use the selectExists
method, but also this method is nonexistent.
Prompt
Enter the follow-up prompt.
the selectExists method is also not available, fix the code properly
Response
The response can be viewed .
Apply Response
The response suggests to use the fetchExists
method. This is already closer to the real solution, but still does not compile. The LLM suggests to use:
boolean exists = dslContext.selectFrom(Customers.CUSTOMERS)
.where(Customers.CUSTOMERS.ID.eq(id))
.fetchExists();
Time to help a little bit and change it manually to the correct implementation.
boolean exists = dslContext.fetchExists(dslContext.selectFrom(Customers.CUSTOMERS));
Run the build, the build is successful.
Prompt
In the current implementation, the repository methods use the jOOQ generated CustomersRecord
as an argument. This means that the service layer would need to know about the repository layer and this is not wanted. The service layer should know only the domain model.
Open a new chat window, add the src
directory to the Prompt Context and also the generated jOOQ classes from the directory target/generated-sources/jooq
. Enter the prompt.
I want that the methods make use of the Customer model and that any mappings
between Customer and CustomerRecord are done in the CustomerRepository itself
Response
The response can be viewed .
Apply Response
This looks great. Some variables, arguments are called record
and this is a reserved word. Change this in customerRecord
.
Run the build, the build is successful.
This took a bit longer than the previous generations, but the end result is quite good.
The changes can be viewed .
Generate Service
In this section, the service will be generated.
Prompt
Open a new chat window and add the full project to the Prompt Context. Enter the prompt.
Create a spring service in order that the operations defined in the openapi spec customers.yaml are supported.
The service must use the CustomerRepository.
Response
The response can be viewed .
Apply Response
The response looks good. Also a Service Interface is created, which is not really needed.
Create package com/mydeveloperplanet/myaicodeprojectplanet/service
and add the Service class and interface.
Run the build, the build is successful.
The changes can be viewed .
Generate Controller
In this section, the controller will be generated.
Prompt
Open a new chat window, add the src
directory and the target/generated-sources/openapi/src
directory to the Prompt Context. Enter the prompt.
Create a Spring Controller in order that the operations defined in the openapi spec customers.yaml are supported.
The controller must implement CustomersApi.
The controller must use the CustomersService.
Response
The response can be viewed .
Apply Response
The response looks good. Create the package com/mydeveloperplanet/myaicodeprojectplanet/controller
and add the controller to this package. Some issues exist:
- The import for
CustomersApi
is missing. Add it. - The arguments in the methods use the
Customer
domain model, which is not correct. It should use the OpenAPICustomer
model.
Prompt
Enter a follow-up prompt.
The interface is not correctly implemented.
The interface must use the openapi Customer model and
must convert it to the Customer domain model which is used by the service.
Response
The response can be viewed .
Apply Response
The response is not correct. The LLM does not seem to see the difference between the Customer
domain model and the Customer
OpenAPI model.
There is also a strange nonexistent Java syntax in the response (looks more like Python).
import com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer as OpenAPICustomer;
Prompt
Enter a follow-up prompt.
This is not correct.
Try again, the openai Customer model is available in package com.mydeveloperplanet.myaicodeprojectplanet.openapi.model,
the domain model is available in package com.mydeveloperplanet.myaicodeprojectplanet.model
Response
The response can be viewed .
This response is identical to the previous one.
Let’s help the LLM a little bit. Fix the methods and replace OpenAPICustomer
with com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer
.
This still raises compile errors, but maybe the LLM can fix this.
Prompt
Open a new chat window, add the src
directory and the target/generated-sources/openapi/src
directory to the Prompt Context. Enter the prompt.
The CustomersController has the following compile errors:
* customersGet return value is not correct
* customersIdGet return value is not correct
Fix this
Response
The response can be viewed .
Apply Response
This seems to be a better solution. Only the following snippet does not compile.
private com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer convertToOpenAPIModel(Customer customer) {
return new com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer(
customer.getId(),
customer.getFirstName(),
customer.getLastName()
);
}
Let’s fix this manually.
private com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer convertToOpenAPIModel(Customer customer) {
com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer openAPICustomer =
new com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer();
openAPICustomer.setId(customer.getId());
openAPICustomer.setFirstName(customer.getFirstName());
openAPICustomer.setLastName(customer.getLastName());
return openAPICustomer;
}
Run the build, the build is successful.
The changes can be viewed .
Run Application
Time to run the application.
Add the following dependency in order to start a PostgreSQL database when running the application.
org.springframework.boot
spring-boot-docker-compose
runtime
true
Add a compose.yaml
file to the root of the repository.
services:
postgres:
image: 'postgres:17-alpine'
environment:
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
- 'POSTGRES_USER=myuser'
labels:
- "org.springframework.boot.service-connection=postgres"
ports:
- '5432'
Run the application.
An error occurs.
2025-02-23T15:29:27.478+01:00 ERROR 33602 --- [MyAiCodeProjectPlanet] [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Liquibase failed to start because no changelog could be found at 'classpath:/db/changelog/db.changelog-master.yaml'.
Action:
Make sure a Liquibase changelog is present at the configured path.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.530 s
[INFO] Finished at: 2025-02-23T15:29:27+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.4.3:run (default-cli) on project myaicodeprojectplanet: Process terminated with exit code: 1 -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
This can be fixed by adding the following line into the application.properties
file.
spring.liquibase.change-log=classpath:db/changelog/db.changelog-root.xml
Run the application and now it starts successfully.
Test Application
The application runs, but is it also functional?
Prompt
Open a new chat window and open the OpenAPI specification. Enter the prompt.
Generate some curl commands in order to test this openapi spec
Response
The response can be viewed .
Run Tests
Create a Customer.
curl -X POST "http://localhost:8080/customers" -H "Content-Type: application/json" -d '{
"firstName": "John",
"lastName": "Doe"
}'
{"timestamp":"2025-02-23T14:33:11.903+00:00","status":404,"error":"Not Found","path":"/customers"}
This test fails. The cause is that the CustomersController
has the following unnecessary annotation.
@RequestMapping("/customers")
This should not be here, this is already part of the CustomersApi
interface.
Remove this line, build the application and run it again.
The changes can be viewed .
Create a Customer. This is successful.
curl -X POST "http://localhost:8080/customers" -H "Content-Type: application/json" -d '{
"firstName": "John",
"lastName": "Doe"
}'
Retrieve a Customer. This is successful.
curl -X GET "http://localhost:8080/customers/1" -H "accept: application/json"
{"id":1,"firstName":"John","lastName":"Doe"}
Update a Customer. This is successful.
curl -X PUT "http://localhost:8080/customers/1" -H "Content-Type: application/json" -d '{
"id": 1,
"firstName": "Jane",
"lastName": "Doe"
}'
Retrieve all Customers. This is successful.
curl -X GET "http://localhost:8080/customers" -H "accept: application/json"
[{"id":1,"firstName":"Jane","lastName":"Doe"}]
Delete a Customer. This is successful.
curl -X DELETE "http://localhost:8080/customers/1" -H "accept: application/json"
Retrieve all Customers. An empty list is returned. This is successful.
curl -X GET "http://localhost:8080/customers" -H "accept: application/json"
[]
Conclusion
It is possible to create a Spring Boot application from scratch using a local LLM. Creating the repository and controller needed some extra iterations and manual interventions. However, the result is quite good—the application is functional and meets the initial requirements.