Upgrade to Pro — share decks privately, control downloads, hide ads and more …

これならできる!Kotlin・Spring・DDDを活用したAll in oneのマイクロサー...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

これならできる!Kotlin・Spring・DDDを活用したAll in oneのマイクロサービス開発術

Avatar for 株式会社出前館

株式会社出前館

June 08, 2025
Tweet

More Decks by 株式会社出前館

Other Decks in Technology

Transcript

  1. build.gradle.ktsイメージ plugins {...} allprojects { project } subprojects { project

    } project(":app-api") {...} project(":app-batch") {...} project(":app-subscriber") {...} project(":usecase") {...} project(":infra") {...} project(":domain-model") {...} project(":test-common") {...} project(":test-common-spring") {...} project(":test-gatling") {...}
  2. @Data data class Shop( val shopId: ShopId, val shopCode: String,

    val shopName: String, val shopImageUrl: String, val shopCategory: String, ) { init { if (shopName.isBlank() || shopImageUrl.isBlank() || shopCategory.isBlank()) { throw DomainModelException(“ShopCopy items must not be Empty.”) } } }
  3. data class Shop( val shopId: ShopId, val shopName: String, val

    shopImageUrl: String, val shopCategory: String, ) { companion object { fun fromApiResponse( shopId: ShopId, shopName: String, shopImageUrl: String, shopCategory: String, ): Shop { return Shop( shopId = shopId, shopName = shopName, shopImageUrl = shopImageUrl, shopCategory = shopCategory, ) } } }
  4. data class Shop( val shopId: ShopId, val shopCode: String, val

    shopName: String, val shopImageUrl: String, val shopCategory: String, ) { init { if (shopName.isBlank() || shopImageUrl.isBlank() || shopCategory.isBlank()) { throw DomainModelException(“ShopCopy items must not be Empty.”) } } }
  5. @Component class AdShopReportGetUseCase( private val hogeMysqlRepository: hogeMysqlRepository, private val fugaExternalApiRepository:

    FugaExternalApiRepository, ) { fun execute(shopIds: List<ShopId>): List<AdShopReport> { val shops = hogeMysqlRepository.getShops( shopIds = shopIds, isExcludeDeleted = true, ) ?: run { throw InvalidInputParameterException( message = "invalid input parameter: shopIds=$shopIds", ) } val reports = fugaExternalApiRepository.getReport( shopIds = shopIds, ) // add more buisiness logic if needed val adShopReports = AdShopReport.listOf( shops = shops, reports = reports, ) return adShopReports } }
  6. • • • • mybatisGenerate // build.gradle.kts val mybatisConfig: Configuration

    by configurations.creating dependencies { mybatisConfig(“org.mybatis.generator:mybatis-generator-core:some-version") mybatisConfig("mysql:mysql-connector-java:some-version") } tasks.register<JavaExec>("mybatisGenerate") { classpath = mybatisConfig mainClass.set("org.mybatis.generator.api.ShellRunner") args = listOf( "-configfile", "path/to/config.xml", "-overwrite" ) }
  7. // define HTTP Interface interface HogeExternalApiClient { @GetExchange("/ad-shops/{ad_shop_id}") fun getAdShops(

    @RequestHeader("Authorization") token: String, @PathVariable("ad_shop_id") adShopId: String, ): ResponseEntity<GetAdShopsResponseData> } // call HTTP Interface @Repository class HogeExternalApiRepository( private val hogeExternalApiClient: HogeExternalApiClient, ) { fun getAdShops(token: String, adShopId: String): List<AdShop> { val response = hogeExternalApiClient.getAdShops(token, adShopId) } }
  8. @Bean fun hogeExternalApiClient(): HogeExternalApiClient { val httpClient = HttpClients .custom()

    .setConnectionManager(PoolingHttpClientConnectionManagerBuilder .create() // setting for Apache connection manager .build()) // setting for Apache Http client .build() val restClient = RestClient.builder() .requestFactory(HttpComponentsClientHttpRequestFactory(httpClient)) .baseUrl("http://localhost:8080") // setting for RestClient .build() return HttpServiceProxyFactory .builderFor(RestClientAdapter.create(restClient)) .build() .createClient(HogeApiClient::class.java) }
  9. • • • • • • • • • •

    • • • openApiGenerate
  10. @Component @ConditionalOnProperty(prefix = "batch", name = ["name"], havingValue = BatchName.HOGE)

    class HogeBatch( private val hogeUseCase: HogeUseCase, ) : ApplicationRunner { override fun run(args: ApplicationArguments) { try { logger.info { "${this.javaClass.simpleName} started." } val start = System.currentTimeMillis() hogeUseCase.execute() val elapsed = System.currentTimeMillis() - start logger.info { "${this.javaClass.simpleName} ended. (${elapsed}ms)" } } catch (ex: Exception) { logger.error(ex) { "${this.javaClass.simpleName} failed." } throw ex } } }
  11. // test class CalculatorTest : StringSpec({ "addition should return correct

    result" { Calculator().add(2, 3) shouldBe 5 } "subtraction should return correct result" { Calculator().subtract(5, 3) shouldBe 2 } }) // test target class Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b }
  12. // test using Should clause class CalculatorTest : ShouldSpec({ should("return

    the correct result when adding two numbers") { Calculator().add(2, 3) shouldBe 5 } should("return the correct result when subtracting two numbers") { Calculator().subtract(5, 3) shouldBe 2 } }) // test target class Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b }
  13. // test like Given-When-Then class CalculatorTest : BehaviorSpec({ given("a calculator")

    { `when`("adding two numbers") { then("it should return the correct result") { Calculator().add(2, 3) shouldBe 5 } } `when`("subtracting two numbers") { then("it should return the correct result") { Calculator().subtract(5, 3) shouldBe 2 } } } }) // test target class Calculator { fun add(a: Int, b: Int): Int = a + b fun subtract(a: Int, b: Int): Int = a - b }
  14. class CalculatorTest : FunSpec({ data class TestPattern( val a: Int,

    val b: Int, val expectedAddition: Int, val expectedSubtraction: Int ) context("Calculator operations") { withData( TestPattern(a = 2, b = 3, expectedAddition = 5, expectedSubtraction = -1), TestPattern(a = 10, b = 5, expectedAddition = 15, expectedSubtraction = 5), TestPattern(a = 0, b = 0, expectedAddition = 0, expectedSubtraction = 0) ) { testCase -> val calculator = Calculator() calculator.add(testCase.a, testCase.b) shouldBe testCase.expectedAddition calculator.subtract(testCase.a, testCase.b) shouldBe testCase.expectedSubtraction } } })
  15. # docker-compose.yml sample-mysql: image: mysql:8.0.28-oracle ports: - “3306:3306” environment: MYSQL_DATABASE:

    sample MYSQL_USER: user001 volumes: - ./sample-mysql:/docker-entrypoint-initdb.d sample-wiremock: image: wiremock/wiremock:3.1.0 ports: - "18180:18180" volumes: - ./sample-wiremock:/home/wiremock command: - "--port=18180" class HogeTest : StringSpec({ val composeContainer = DockerComposeContainer(File(“../tool/docker-compose.yml”)) .withExposedService(“sample-mysql”, 3306, Wait.forListeningPort()) .withExposedService(“sample-wiremock”, 18180, Wait.forListeningPort()) .apply { start() } “sample test" { 1 + 1 shouldBe 2 } })
  16. // libs.versions.toml [versions] gatling = "3.13.5” [plugins] gatling = {

    id = "io.gatling.gradle", version.ref = "gatling" } [libraries] gatling-charts-highcharts = { module = "io.gatling.highcharts:gatling-charts-highcharts", version.ref = "gatling" } gatling-app = { module = "io.gatling:gatling-app", version.ref = "gatling" } // build.gradle.kts project(":test-gatling") { apply(plugin = rootProject.libs.plugins.gatling.get().pluginId) dependencies { gatling(rootProject.libs.gatling.charts.highcharts) gatling(rootProject.libs.gatling.app) } gatling { System.getenv().forEach { environment.put(it.key, it.value) // pass environment variables to gatling app } } }
  17. class HogeApiSimulations : Simulation() { val scenario1 = scenario(“scenario1”) .exec(http(“hoge-api-request”)

    .post(“/v1/hoges”) .body(StringBody(“”“{”key1“: ”XXX“, “key2”: 999}“”“.trimMargin())) .check(status().shouldBe(200)) ) val httpProtocol = http.baseUrl(”https://hoge.sample.com“) .header(”content-type“, ”application/json“) .header(”X-Api-Key“, System.getenv(”X_API_KEY“)) init { setUp( scenario1.injectOpen(rampUsersPerSec(10.0).to(100.0).during(60)) .andThen(scenario1.injectOpen(constantUsersPerSec(100.0).during(300))) ).protocols(httpProtocol) } }
  18. # gradle/libs.versions.toml [versions] javaLanguageVersion = “21” springBoot = “3.4.0” springRetry

    = "2.0.11" [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } [libraries] spring-retry = { module = "org.springframework.retry:spring-retry", version.ref = "springRetry" } spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springBoot" } spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } [bundles] spring-boot-setting = [ "spring-boot-starter", "spring-boot-starter-web”, "spring-retry" ]
  19. # build.gradle.kts project(":app-api") { kotlin { jvmToolchain { this.languageVersion.set(JavaLanguageVersion.of(rootProject.libs.versions.javaLanguageVersion.get())) }

    apply(plugin = rootProject.libs.plugins.spring.boot.get().pluginId) dependencies { implementation(rootProject.libs.bundles.spring.boot.setting) }
  20. // ControllerExtensions.kt fun HogesPostRequest.toDomainModel(): Hoge { return HogeModel( hogeId =

    HogeId(this.hogeId), fuga = Fuga.of(this.fuga), ) } // HogeController.kt @RestController class HogeController(private val hogeUseCase: HogeUseCase) : HogesApi { override fun hogesPost(hogesPostRequest: HogesPostRequest): ResponseEntity<HogesPostResponse> { hogeUseCase.create(hogesPostRequest.toDomainModel()) return ResponseEntity.ok(HogesPostResponse(code = "OK")) } }
  21. // build.gradle.kts tasks.register<Exec>("dockerComposeUp") { commandLine("docker-compose", "-f", "./docker-compose-test.yml", "up", "-d") }

    tasks.register<Exec>("dockerComposeDown") { commandLine("docker-compose", "-f", "./docker-compose-test.yml", "down", "--rmi", "—volumes") } tasks.named<Test>("test") { dependsOn("dockerComposeUp") finalizedBy("dockerComposeDown") }
  22. // libs.versions.toml [libraries] opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api" }

    // build.gradle.kts project(":sample") { dependencies { implementation(rootProject.libs.opentelemetry.api) } } //OpenTelemetryConfig.kt @Configuration class OpenTelemetryConfig { @Bean fun openTelemetry(): OpenTelemetry { return GlobalOpenTelemetry.get() } } // execute command java -javaagent:/path/to/the/opentelemetry-javaagent.jar –jar sample.jar