Kotlin-Project
I'll create a step-by-step procedure to initialize a Spring Boot project with Gradle and Kotlin on your MacOS M1 machine. Here's a CLI script that you can follow:
First, ensure you have the necessary tools installed:
# Check if Java is installed (you should use JDK 17 or later)
java -version
# Install Java via Homebrew if needed
# brew install openjdk@17
# Check if SDKMAN is installed (recommended for managing JDK versions)
sdk version
# Install SDKMAN if needed
# curl -s "https://get.sdkman.io" | bash
# source "$HOME/.sdkman/bin/sdkman-init.sh"
# sdk install java 17.0.7-tem
# Install Spring Boot CLI via SDKMAN
sdk install springboot
# Or via Homebrew
# brew install spring-boot
# Create a directory for your project
mkdir insure-api
cd insurance-api
# Use curl to download a pre-configured project from Spring Initializr
curl https://start.spring.io/starter.zip \
-d type=gradle-project \
-d language=kotlin \
-d bootVersion=3.4.3 \
-d baseDir=insure-api \
-d groupId=net.thava.insure \
-d artifactId=insure-api \
-d name=insure-api \
-d description="Insurance Financial Products API" \
-d packageName=net.thava.insure\
-d packaging=jar \
-d javaVersion=17 \
-d dependencies=web,data-jpa,postgresql,validation,security,actuator,flyway,lombok,devtools \
-o insure-api.zip
# Unzip the downloaded file
unzip insure-api.zip
# Remove the zip file
rm insure-api.zip
# Open the project directory (if you're not already in it)
cd insurance-finance-api
# Create a settings file for Gradle version catalogs
mkdir -p gradle
cat << 'EOF' > gradle/libs.versions.toml
[versions]
kotlin = "1.9.22"
spring-boot = "3.2.0"
spring-cloud = "2023.0.0"
spring-dependency-management = "1.1.4"
testcontainers = "1.19.3"
mockk = "1.13.8"
kotest = "5.8.0"
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" }
spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation" }
spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" }
spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" }
spring-boot-devtools = { module = "org.springframework.boot:spring-boot-devtools" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" }
postgresql = { module = "org.postgresql:postgresql" }
flyway-core = { module = "org.flywaydb:flyway-core" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" }
EOF
# Update build.gradle.kts to use the version catalog
cat << 'EOF' > build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.kotlin.jpa)
}
group = "com.yourcompany.insurance"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
}
dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.data.jpa)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.security)
implementation(libs.spring.boot.starter.actuator)
implementation(libs.flyway.core)
implementation(libs.kotlin.reflect)
implementation(libs.kotlin.stdlib)
developmentOnly(libs.spring.boot.devtools)
runtimeOnly(libs.postgresql)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.mockk)
testImplementation(libs.kotest.runner.junit5)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.testcontainers)
testImplementation(libs.testcontainers.postgresql)
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.bootJar {
archiveFileName.set("${archiveBaseName.get()}.${archiveExtension.get()}")
}
// For direct dependency declarations without version catalog
dependencies {
// Additional test dependencies to ensure they're properly resolved
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.kotest:kotest-assertions-core:5.8.0")
}
EOF
### 5. Create an application.yml file
```bash
# Create a proper YAML configuration file instead of properties
mkdir -p src/main/resources
cat << 'EOF' > src/main/resources/application.yml
spring:
application:
name: insurance-finance-api
datasource:
url: jdbc:postgresql://localhost:5432/insurance_finance
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
security:
user:
name: admin
password: admin
server:
port: 8080
servlet:
context-path: /api/v1
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
logging:
level:
com.yourcompany.insurance: DEBUG
org.springframework: INFO
org.hibernate: INFO
EOF
# Create package structure
mkdir -p src/main/kotlin/net/thava/insure/{controller,service,repository,model,config,exception}
# Create a basic entity class
cat << 'EOF' > src/main/kotlin/net/thava/insure/model/InsuranceProduct.kt
package net.thava.insure.finance.model
import jakarta.persistence.*
import java.math.BigDecimal
import java.time.LocalDateTime
@Entity
@Table(name = "insurance_products")
data class InsuranceProduct(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val code: String,
@Column(nullable = false, length = 1000)
val description: String,
@Column(nullable = false)
val premiumAmount: BigDecimal,
@Column(nullable = false)
val coverageAmount: BigDecimal,
@Column(nullable = false)
val term: Int, // in months
@Column(nullable = false)
val riskCategory: String,
@Column(nullable = false)
val isActive: Boolean = true,
@Column(nullable = false)
val createdAt: LocalDateTime = LocalDateTime.now(),
@Column(nullable = true)
val updatedAt: LocalDateTime? = null
)
EOF
# Create a repository interface
cat << 'EOF' > src/main/kotlin/com/yourcompany/insurance/finance/repository/InsuranceProductRepository.kt
package com.yourcompany.insurance.finance.repository
import com.yourcompany.insurance.finance.model.InsuranceProduct
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.math.BigDecimal
@Repository
interface InsuranceProductRepository : JpaRepository<InsuranceProduct, Long> {
fun findByCode(code: String): InsuranceProduct?
fun findByIsActiveTrue(): List<InsuranceProduct>
fun findByPremiumAmountLessThan(amount: BigDecimal): List<InsuranceProduct>
}
EOF
# Create a service class
cat << 'EOF' > src/main/kotlin/com/yourcompany/insurance/finance/service/InsuranceProductService.kt
package com.yourcompany.insurance.finance.service
import com.yourcompany.insurance.finance.model.InsuranceProduct
import com.yourcompany.insurance.finance.repository.InsuranceProductRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDateTime
@Service
class InsuranceProductService(private val insuranceProductRepository: InsuranceProductRepository) {
fun getAllProducts(): List<InsuranceProduct> {
return insuranceProductRepository.findAll()
}
fun getActiveProducts(): List<InsuranceProduct> {
return insuranceProductRepository.findByIsActiveTrue()
}
fun getProductById(id: Long): InsuranceProduct {
return insuranceProductRepository.findById(id)
.orElseThrow { RuntimeException("Insurance product not found with id: $id") }
}
fun getProductByCode(code: String): InsuranceProduct? {
return insuranceProductRepository.findByCode(code)
}
@Transactional
fun createProduct(product: InsuranceProduct): InsuranceProduct {
return insuranceProductRepository.save(product)
}
@Transactional
fun updateProduct(id: Long, updatedProduct: InsuranceProduct): InsuranceProduct {
val existingProduct = getProductById(id)
val productToUpdate = existingProduct.copy(
name = updatedProduct.name,
code = updatedProduct.code,
description = updatedProduct.description,
premiumAmount = updatedProduct.premiumAmount,
coverageAmount = updatedProduct.coverageAmount,
term = updatedProduct.term,
riskCategory = updatedProduct.riskCategory,
isActive = updatedProduct.isActive,
updatedAt = LocalDateTime.now()
)
return insuranceProductRepository.save(productToUpdate)
}
@Transactional
fun deleteProduct(id: Long) {
insuranceProductRepository.deleteById(id)
}
fun getProductsUnderPremium(amount: BigDecimal): List<InsuranceProduct> {
return insuranceProductRepository.findByPremiumAmountLessThan(amount)
}
}
EOF
# Create a controller
cat << 'EOF' > src/main/kotlin/com/yourcompany/insurance/finance/controller/InsuranceProductController.kt
package com.yourcompany.insurance.finance.controller
import com.yourcompany.insurance.finance.model.InsuranceProduct
import com.yourcompany.insurance.finance.service.InsuranceProductService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.math.BigDecimal
@RestController
@RequestMapping("/products")
class InsuranceProductController(private val insuranceProductService: InsuranceProductService) {
@GetMapping
fun getAllProducts(): ResponseEntity<List<InsuranceProduct>> {
return ResponseEntity.ok(insuranceProductService.getAllProducts())
}
@GetMapping("/active")
fun getActiveProducts(): ResponseEntity<List<InsuranceProduct>> {
return ResponseEntity.ok(insuranceProductService.getActiveProducts())
}
@GetMapping("/{id}")
fun getProductById(@PathVariable id: Long): ResponseEntity<InsuranceProduct> {
return ResponseEntity.ok(insuranceProductService.getProductById(id))
}
@GetMapping("/code/{code}")
fun getProductByCode(@PathVariable code: String): ResponseEntity<InsuranceProduct> {
val product = insuranceProductService.getProductByCode(code)
return if (product != null) {
ResponseEntity.ok(product)
} else {
ResponseEntity.notFound().build()
}
}
@GetMapping("/premium-under/{amount}")
fun getProductsUnderPremium(@PathVariable amount: BigDecimal): ResponseEntity<List<InsuranceProduct>> {
return ResponseEntity.ok(insuranceProductService.getProductsUnderPremium(amount))
}
@PostMapping
fun createProduct(@RequestBody product: InsuranceProduct): ResponseEntity<InsuranceProduct> {
return ResponseEntity.status(HttpStatus.CREATED).body(insuranceProductService.createProduct(product))
}
@PutMapping("/{id}")
fun updateProduct(
@PathVariable id: Long,
@RequestBody product: InsuranceProduct
): ResponseEntity<InsuranceProduct> {
return ResponseEntity.ok(insuranceProductService.updateProduct(id, product))
}
@DeleteMapping("/{id}")
fun deleteProduct(@PathVariable id: Long): ResponseEntity<Unit> {
insuranceProductService.deleteProduct(id)
return ResponseEntity.noContent().build()
}
}
EOF
# Create a database migration script
mkdir -p src/main/resources/db/migration
cat << 'EOF' > src/main/resources/db/migration/V1__Create_insurance_products_table.sql
CREATE TABLE insurance_products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
code VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(1000) NOT NULL,
premium_amount DECIMAL(19, 2) NOT NULL,
coverage_amount DECIMAL(19, 2) NOT NULL,
term INT NOT NULL,
risk_category VARCHAR(50) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP
);
-- Add index for commonly queried fields
CREATE INDEX idx_insurance_products_code ON insurance_products(code);
CREATE INDEX idx_insurance_products_is_active ON insurance_products(is_active);
EOF
# Create the main application class
cat << 'EOF' > src/main/kotlin/com/yourcompany/insurance/finance/InsuranceFinanceApiApplication.kt
package com.yourcompany.insurance.finance
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class InsuranceFinanceApiApplication
fun main(args: Array<String>) {
runApplication<InsuranceFinanceApiApplication>(*args)
}
EOF
# Create test directory structure
mkdir -p src/test/kotlin/com/yourcompany/insurance/finance/{controller,service,repository}
# Create a service test
cat << 'EOF' > src/test/kotlin/com/yourcompany/insurance/finance/service/InsuranceProductServiceTest.kt
package com.yourcompany.insurance.finance.service
import com.yourcompany.insurance.finance.model.InsuranceProduct
import com.yourcompany.insurance.finance.repository.InsuranceProductRepository
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.Optional
class InsuranceProductServiceTest {
private val repository: InsuranceProductRepository = mockk()
private val service = InsuranceProductService(repository)
@Test
fun `should return all products`() {
// Given
val products = listOf(
InsuranceProduct(
id = 1,
name = "Term Life Insurance",
code = "TLI001",
description = "Basic term life insurance product",
premiumAmount = BigDecimal("100.00"),
coverageAmount = BigDecimal("100000.00"),
term = 12,
riskCategory = "LOW"
)
)
every { repository.findAll() } returns products
// When
val result = service.getAllProducts()
// Then
result shouldBe products
verify(exactly = 1) { repository.findAll() }
}
@Test
fun `should return product by id`() {
// Given
val product = InsuranceProduct(
id = 1,
name = "Term Life Insurance",
code = "TLI001",
description = "Basic term life insurance product",
premiumAmount = BigDecimal("100.00"),
coverageAmount = BigDecimal("100000.00"),
term = 12,
riskCategory = "LOW"
)
every { repository.findById(1) } returns Optional.of(product)
// When
val result = service.getProductById(1)
// Then
result shouldBe product
verify(exactly = 1) { repository.findById(1) }
}
@Test
fun `should throw exception when product not found`() {
// Given
every { repository.findById(any()) } returns Optional.empty()
// When/Then
shouldThrow<RuntimeException> {
service.getProductById(1)
}
verify(exactly = 1) { repository.findById(1) }
}
}
EOF
# Test the application
./gradlew test
# Build the application
./gradlew build
# Run the application (you'll need PostgreSQL running on your machine)
# ./gradlew bootRun
# Create a docker-compose file for local development
cat << 'EOF' > docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: insurance-finance-postgres
ports:
- "5432:5432"
environment:
POSTGRES_DB: insurance_finance
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
EOF
# Start the PostgreSQL container
docker-compose up -d
# Run the Spring Boot application
./gradlew bootRun
That's it! You've initialized a modern Spring Boot project with:
The project follows modern practices including:
You can access your API at http://localhost:8080/api/v1/products once it's running.