Getting Started with Spring Boot and Kotlin: A Complete Guide for Java Developers

January 6, 2025 7 min read Intermediate

Spring Boot has become the de facto standard for building web applications in the Java ecosystem. With Kotlin's rising popularity and its excellent interoperability with Java, more developers are choosing Kotlin for their Spring Boot applications. This guide will walk you through creating your first Spring Boot application with Kotlin, highlighting key differences from Java and explaining Kotlin-specific features that make Spring Boot development more enjoyable.

By following this tutorial, you'll build a simple book management API that demonstrates essential Spring Boot concepts using Kotlin. We'll cover everything from setting up your development environment to implementing a complete REST API with database integration and tests.

What you'll learn

  • How to set up a Spring Boot project with Kotlin
  • Understanding key Kotlin features that make Spring Boot development easier
  • Building a complete REST API for managing books
  • Working with databases using Spring Data JPA
  • Writing tests for your Spring Boot application
  • Best practices for error handling
  • Estimated reading time: 20 minutes

Prerequisites

Before we begin, make sure you have:

  • Java Development Kit (JDK) 17 or later installed
  • An Integrated Development Environment (IDE) - IntelliJ IDEA is recommended for Kotlin development
  • Basic knowledge of Java and Spring Boot concepts
  • Basic understanding of REST APIs

🛠️ Setting Up Your Development Environment

Let's start by creating a new Spring Boot project with Kotlin. While you can use Spring Initializr through your IDE, we'll use the web interface at https://start.spring.io/ for clarity.

  1. Visit https://start.spring.io/
  2. Configure your project with these settings:
    • Project: Gradle - Kotlin
    • Language: Kotlin
    • Spring Boot: 3.3.6 (latest stable version)
    • Project Metadata:
      • Group: dev.kotlincraft
      • Artifact: bookapi
      • Name: bookapi
      • Description: Book Management API with Kotlin and Spring Boot
      • Package name: dev.kotlincraft.bookapi
      • Packaging: Jar
      • Java: 17
  3. Add the following dependencies:
    • Spring Web
    • Spring Data JPA
    • H2 Database
    • Validation

Click "Generate" to download the project. Extract the ZIP file and open it in your IDE.

📦 Understanding the Project Structure

After opening the project, you'll see a structure similar to a Java Spring Boot project, but with some Kotlin-specific elements:

src/
├── main/
│   ├── kotlin/
│   │   └── com/
│   │       └── example/
│   │           └── bookapi/
│   │               └── BookapiApplication.kt
│   └── resources/
│       └── application.properties
└── test/
    └── kotlin/
        └── com/
            └── example/
                └── bookapi/
                    └── BookapiApplicationTests.kt

The main difference in folder structure you'll notice is that source files are under kotlin directories instead of java directories.

🧱 Key Kotlin Features for Spring Boot Development

Before diving into the implementation, let's understand some Kotlin features that make Spring Boot development more efficient. However, this is not required.

Data Classes

In Java, creating a simple POJO (without the help of lombok) requires a lot of boilerplate code:

public class Book {
    private Long id;
    private String title;
    private String author;
    private int publishYear;

    // Constructor
    public Book(Long id, String title, String author, int publishYear) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.publishYear = publishYear;
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    // ... more getters and setters

    // equals(), hashCode(), toString()
    // ... more boilerplate
}

Kotlin simplifies this dramatically with data classes:

data class Book(
    val id: Long? = null,
    val title: String,
    val author: String,
    val publishYear: Int
)

This single declaration provides:

  • All properties
  • Constructor
  • equals()/hashCode()
  • toString()
  • copy() function
  • Component functions for destructuring

Constructor Property Declaration

Kotlin allows you to declare properties directly in the primary constructor. This is particularly useful for Spring components:

@Service
class BookService(private val bookRepository: BookRepository)

This single line declares:

  • A class property bookRepository
  • A constructor parameter
  • Automatic injection of the repository

In Java, the equivalent code would require significantly more boilerplate:

@Service
public class BookService {
    private final BookRepository bookRepository;

    // Constructor injection
    @Autowired  // Optional in newer Spring versions
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

Null Safety

Kotlin's type system distinguishes between nullable and non-nullable types. This helps prevent the infamous NullPointerException:

// Non-nullable String - can't be null
var title: String = "Clean Code"

// Nullable String - can be null
var description: String? = null

// Won't compile - can't assign null to non-nullable type
title = null // Compilation error

// Safe call operator
description?.length // Returns null if description is null

// Elvis operator for default values
val length = description?.length ?: 0

🔄 Building the Book Management API

Now that we understand the basics, let's build our API. We'll create a simple CRUD API for managing books.

Setting Up the Database Configuration

First, configure the H2 database in application.properties:

spring.datasource.url=jdbc:h2:mem:bookdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true

Creating the Book Entity

Create a new file Book.kt:

package com.example.bookapi.model

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Positive

@Entity // Tells Spring this is a database table
data class Book(
    @Id // Marks this as the primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-generates IDs
    val id: Long? = null, // Nullable because it's generated by database
    
    @field:NotBlank(message = "Title is required") // Validation rule
    val title: String, // Non-nullable String - must always have a value
    

    @field:NotBlank(message = "Author is required")
    val author: String,

    @field:Positive(message = "Publish year must be positive")
    val publishYear: Int
)

Notice how we combine the data class feature with JPA annotations. The @field: prefix is necessary for validation annotations in Kotlin to target the field rather than the constructor parameter.

Implementing the Repository

Create BookRepository.kt:

package com.example.bookapi.repository

import com.example.bookapi.model.Book
import org.springframework.data.jpa.repository.JpaRepository

interface BookRepository : JpaRepository<Book, Long>

Thanks to Spring Data JPA, this simple interface provides all basic CRUD operations.

Creating the Service Layer

Create BookService.kt:

package com.example.bookapi.service

import com.example.bookapi.model.Book
import com.example.bookapi.repository.BookRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class BookService(private val bookRepository: BookRepository) {

    fun findAll(): List<Book> = bookRepository.findAll()

    fun findById(id: Long): Book = bookRepository.findById(id)
        .orElseThrow { NoSuchElementException("Book not found with id: $id") }

    fun create(book: Book): Book = bookRepository.save(book)

    fun update(id: Long, book: Book): Book {
        return if (bookRepository.existsById(id)) {
            bookRepository.save(book.copy(id = id))
        } else {
            throw NoSuchElementException("Book not found with id: $id")
        }
    }

    fun delete(id: Long) {
        if (!bookRepository.existsById(id)) {
            throw NoSuchElementException("Book not found with id: $id")
        }
        bookRepository.deleteById(id)
    }
}

Note how we use Kotlin's expression bodies (fun findAll() = ...) for concise one-line functions and the copy() function provided by the data class for updating entities.

Building the REST Controller

Create BookController.kt:

package com.example.bookapi.controller

import com.example.bookapi.model.Book
import com.example.bookapi.service.BookService
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/books")
class BookController(private val bookService: BookService) {

    @GetMapping
    fun getAllBooks(): List<Book> = bookService.findAll()

    @GetMapping("/{id}")
    fun getBook(@PathVariable id: Long): Book = bookService.findById(id)

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun createBook(@Valid @RequestBody book: Book): Book = bookService.create(book)

    @PutMapping("/{id}")
    fun updateBook(
        @PathVariable id: Long,
        @Valid @RequestBody book: Book
    ): Book = bookService.update(id, book)

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    fun deleteBook(@PathVariable id: Long) = bookService.delete(id)
}

Adding Error Handling

Create ErrorHandler.kt to handle exceptions globally:

package com.example.bookapi.controller

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class ErrorHandler {

    @ExceptionHandler(NoSuchElementException::class)
    fun handleNotFound(e: NoSuchElementException): ResponseEntity<Map<String, String>> =
        ResponseEntity(mapOf("error" to (e.message ?: "Not Found")), HttpStatus.NOT_FOUND)

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationErrors(e: MethodArgumentNotValidException): ResponseEntity<Map<String, List<String>>> {
        val errors = e.bindingResult.fieldErrors
            .map { "${it.field}: ${it.defaultMessage}" }
        return ResponseEntity(mapOf("errors" to errors), HttpStatus.BAD_REQUEST)
    }
}

Testing Your Application

Create BookServiceTest.kt:

package com.example.bookapi.service

import com.example.bookapi.model.Book
import com.example.bookapi.repository.BookRepository
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.transaction.annotation.Transactional

@SpringBootTest
@Transactional
class BookServiceTest {

    @Autowired
    private lateinit var bookService: BookService

    @Test
    fun `should create new book`() {
        val book = Book(
            title = "The Kotlin Programming Language",
            author = "JetBrains",
            publishYear = 2021
        )

        val savedBook = bookService.create(book)
        
        assertNotNull(savedBook.id)
        assertEquals(book.title, savedBook.title)
        assertEquals(book.author, savedBook.author)
        assertEquals(book.publishYear, savedBook.publishYear)
    }

    @Test
    fun `should throw exception when book not found`() {
        assertThrows(NoSuchElementException::class.java) {
            bookService.findById(-1)
        }
    }
}

Note Kotlin's string template feature in the test names (using backticks for spaces) and the concise assertion syntax.

✅ Running and Testing the API

  1. Start the application:
./gradlew bootRun
  1. Test the API using curl or any HTTP client:

Create a book:

curl -X POST http://localhost:8080/api/books \
     -H "Content-Type: application/json" \
     -d '{"title":"The Kotlin Programming Language","author":"JetBrains","publishYear":2021}'

Get all books:

curl http://localhost:8080/api/books

Common Issues and Solutions

    • Problem: @Autowired properties remain null
    • Solution: Use constructor injection or lateinit modifier:
    • Problem: JPA requires a no-arg constructor
    • Solution: Add @Entity annotation and make properties var or provide default values:

JPA Entity Issues

@Entity
data class Book(
    @Id @GeneratedValue
    val id: Long? = null,
    var title: String? = null  // Use var for JPA
)

Property/Field Injection Not Working

@Autowired
private lateinit var service: BookService

Next Steps

Now that you have a basic understanding of Spring Boot with Kotlin, you can:

  • Add more complex relationships between entities
  • Implement user authentication and authorization
  • Add pagination and sorting
  • Implement caching
  • Add API documentation with OpenAPI/Swagger

Conclusion

You've successfully created a Spring Boot application with Kotlin, implementing a complete REST API with database integration and tests. Kotlin's features like data classes, null safety, and concise syntax make Spring Boot development more enjoyable and less error-prone.

The complete source code for this tutorial is available on GitHub.


📚 Key Terms Explained

  • Entity: A class that represents a database table
  • Repository: A class that handles database operations
  • DTO: Data Transfer Object - represents data structure for API
  • REST: Representational State Transfer - architectural style for APIs

For more advanced topics, check out our other guides:

Latest Articles