$30 off During Our Annual Pro Sale. View Details »

Microservices with Kotlin and Ktor

Microservices with Kotlin and Ktor

Ktor is a Kotlin framework for creating web servers (and clients).
Unlike other frameworks, Ktor is written entirely in Kotlin and can therefore use language features such as Coroutines, reified Generics and Extension Functions more effectively.
With little overhead, fast results and good performance, Ktor is especially interesting for microservices.
If you are bored with Spring, you will find what you are looking for here. All others learn at least something new.

by Lovis
presented on May 2, 2019 @car2go

More Decks by Kotlin User Group Hamburg

Other Decks in Programming

Transcript

  1. Microservices with Lovis Möller @lovisbrot lovis.moeller@codecentric.de Kotlin and Ktor

  2. Microservices — still a thing? https://www.amazon.de/Building-Microservices-Designing-Fine-Grained-Systems/dp/1492034029/

  3. It depends.

  4. Quickly adapt to changes of a fast-moving market.

  5. Quickly adapt to changes of a fast-moving market. Works best

    with multiple small teams.
  6. Quickly adapt to changes of a fast-moving market. Works best

    with multiple small teams. Not cheap.
  7. Ok! Microservices it is. Let’s do it with Kotlin and

    Ktor.
  8. • Consice, Safe, Pragmatic and tool-friendly • Created by JetBrains

    • Runs on the JVM, multi-platform and native. • 100% interoperable with Java • Open Source Kotlin
  9. • Framework for Webservers (and -clients) • Written in Kotlin

    • Coroutines, Extension functions, internal DSLs • Open Source • https://ktor.io/quickstart/ Ktor
  10. fun main() { embeddedServer(Netty, port = 8080) { routing {

    get("/") { call.respondText("Hello World!") } } }.start(wait = true) }
  11. fun main() { embeddedServer(Netty, port = 8080) { routing {

    get("/") { call.respondText("Hello World!") } } }.start(wait = true) }
  12. fun main() { embeddedServer(Netty, port = 8080) { routing {

    get("/") { call.respondText("Hello World!") } } }.start(wait = true) }
  13. fun main() { embeddedServer(Netty, port = 8080) { routing {

    get("/") { call.respondText("Hello World!") } } }.start(wait = true) } Named Arguments
  14. fun main() { embeddedServer(Netty, port = 8080) { routing {

    get("/") { call.respondText("Hello World!") } } }.start(wait = true) }
  15. fun main() { embeddedServer(Netty, port = 8080) { routing {

    get("/") { call.respondText("Hello World!") } } }.start(wait = true) }
  16. fun main() { embeddedServer( Netty, port = 8080, module =

    Application mainModule ).start(wait = true) }
  17. fun main() { embeddedServer( Netty, port = 8080, module =

    Application module ).start(wait = true) }
  18. fun Application.module() { routing { get("/") { call.respondText("Hello World!") }

    } }
  19. fun Application.module() { routing { get("/") { call.respondText("Hello World!") }

    } } Extension Function
  20. fun Application.module() { routing { get("/") { call.respondText("Hello World!") }

    } }
  21. fun Application.module() { this.routing { get("/") { call.respondText("Hello World!") }

    } }
  22. fun Application.module() { this.routing { get("/") { call.respondText("Hello World!") }

    } }
  23. fun Application.module() { this.routing { get("/") { call.respondText("Hello World!") }

    } } this == Application
  24. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  25. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  26. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  27. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  28. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  29. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  30. fun shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

    Error!
  31. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  32. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  33. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  34. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

  35. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

    val shuffled = "techcamp".shuffle() println(shuffled)
  36. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

    $ mactceph val shuffled = "techcamp".shuffle() println(shuffled)
  37. Microservices Microservice ≈ Bounded Context Domain for this talk: Cat

    food configurator https://github.com/lmller/ktor-microservices-sample Sample code at
  38. Cat food configurator Configure Food Add to cart Checkout Ingredient-warehouse

    maintenance Shipment Customer Self-Care
  39. Cat food configurator Configure Food Add to cart Checkout Shipment

    Customer Warehouse employee Backoffice employee Ingredient-warehouse maintenance Customer Self-Care
  40. Cat food configurator Configure Food Add to cart Checkout Shipment

    Customer Warehouse employee Backoffice employee Ingredient-warehouse maintenance Customer Self-Care Potential Users
  41. Cat food configurator Configure Food Add to cart Checkout Shipment

    Ingredient-warehouse maintenance Customer Self-Care Potential Communication
  42. Cat food configurator Configure Food Add to cart Checkout Shipment

    Ingredient-warehouse maintenance Customer Self-Care
  43. None
  44. fun Application.module() { val db = WarehouseDB() routing { get("/stock")

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  45. fun Application.module() { val db = WarehouseDB() routing { get("/stock")

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  46. fun Application.module() { val db = WarehouseDB() routing { get("/stock")

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  47. fun Application.module() { val db = WarehouseDB() routing { get("/stock")

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  48. fun Application.module() { val db = WarehouseDB() routing { get("/stock")

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } 500 InternalServerError Warehouse
  49. fun Application.module() { val db = WarehouseDB() routing { get("/stock")

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  50. fun Application.module() {  routing { get("/stock") { call.respond(warehouse.stock.map {

    (k, v)  StockDto(k, v) }) }  Warehouse
  51. fun Application.module() { install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } }

     routing { get("/stock") { call.respond(warehouse.stock.map { (k, v)  StockDto(k, v) }) }  Warehouse
  52. fun Application.module() { install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } }

     routing { get("/stock") { call.respond(warehouse.stock.map { (k, v)  StockDto(k, v) }) }  Warehouse
  53. fun Application.module() { install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } }

     routing { get("/stock") { call.respond(warehouse.stock.map { (k, v)  StockDto(k, v) }) }  [ { "name" : "Beef","quantity" : 100 }, { "name" : "Lamb","quantity" : 50 }, { "name" : "Fish","quantity" : 0 } ] Warehouse
  54. get(" ") { }

  55. get(" ") { } post(" ") { }

  56. get(" ") { } post(" ") { } put(" ")

    { }
  57. get(" ") { } post(" ") { } put(" ")

    { } patch(" ") { }
  58. get(" ") { } post(" ") { } put(" ")

    { } patch(" ") { } delete(" ") { }
  59. get(" ") { } post(" ") { } put(" ")

    { } patch(" ") { } delete(" ") { } head(" ") { }
  60. get(" ") { } post(" ") { } put(" ")

    { } patch(" ") { } delete(" ") { } head(" ") { } options(" ") { }
  61. get("/foo/bar") { } get("/foo/baz") { }

  62. get("/foo/bar") { } get("/foo/baz") { } route("/foo") { get("/bar") {

    } get("/baz") { } }
  63. get("/foo/bar") { } get("/foo/baz") { } route("/foo") { get("/bar") {

    } get("/baz") { } } https://ktor.io/servers/features/routing.html#routing-tree
  64. fun Application.module() { install(ContentNegotiation) { … } val db =

    WarehouseDB() routing { get("/stock") { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  65. fun Application.module() { install(ContentNegotiation) { … } val db =

    WarehouseDB() routing { get("/stock") { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  66. private object StockTable : UUIDTable() { val itemName = varchar("itemName",

    128).uniqueIndex() val quantity = integer("quantity") } Warehouse
  67. private object StockTable : UUIDTable() { val itemName = varchar("itemName",

    128).uniqueIndex() val quantity = integer("quantity") } Warehouse
  68. private object StockTable : UUIDTable() { val itemName = varchar("itemName",

    128).uniqueIndex() val quantity = integer("quantity") } Warehouse
  69. private object StockTable : UUIDTable() { val itemName = varchar("itemName",

    128).uniqueIndex() val quantity = integer("quantity") }  insert { column -> column[itemName] = "Fish" column[quantity] = 100 } Warehouse
  70. private object StockTable : UUIDTable() { val itemName = varchar("itemName",

    128).uniqueIndex() val quantity = integer("quantity") }  insert { column -> column[itemName] = "Fish" column[quantity] = 100 }  StockTable.selectAll() .map { it[StockTable.itemName] to it[StockTable.quantity] } .toMap() Warehouse
  71. Warehouse private object StockTable : UUIDTable() { val itemName =

    varchar("itemName", 128).uniqueIndex() val quantity = integer("quantity") }  insert { column -> column[itemName] = "Fish" column[quantity] = 100 }  StockTable.selectAll() .map { it[StockTable.itemName] to it[StockTable.quantity] } .toMap() https://github.com/JetBrains/Exposed
  72. fun Application.module() { install(ContentNegotiation) { … } routing { get("/stock")

    { … } put("/stock/{item}") { } } Warehouse
  73. fun Application.module() { install(ContentNegotiation) { … } routing { get("/stock")

    { … } put("/stock/{item}") { val itemName = call.parameters.getOrFail("item") val stock = call.receive<StockDto>() db.update(itemName, stock.quantity ?: 0) call.respond(OK) } } Warehouse
  74. fun Application.module() { install(ContentNegotiation) { … } routing { get("/stock")

    { … } put("/stock/{item}") { val itemName = call.parameters.getOrFail("item") val stock = call.receive<StockDto>() db.update(itemName, stock.quantity ?: 0) call.respond(OK) } } Warehouse
  75. fun Application.module() { install(ContentNegotiation) { … } routing { get("/stock")

    { … } put("/stock/{item}") { val itemName = call.parameters.getOrFail("item") val stock = call.receive<StockDto>() db.update(itemName, stock.quantity ?: 0) call.respond(OK) } } Warehouse
  76. fun Application.module() { install(ContentNegotiation) { … } routing { get("/stock")

    { … } put("/stock/{item}") { val itemName = call.parameters.getOrFail("item") val stock = call.receive<StockDto>() db.update(itemName, stock.quantity ?: 0) call.respond(OK) } } Warehouse
  77. BASIC AUTH

  78. fun Application.module() { install(ContentNegotiation) { … } install(Authentication) { }

    Warehouse
  79. fun Application.module() { install(ContentNegotiation) { … } install(Authentication) { basic

    { validate { (user, pw)  if (user  "not-a-hacker" && pw  "supersecure!") UserIdPrincipal(user) else null } } } Warehouse
  80. fun Application.module() { install(ContentNegotiation) { … } install(Authorization) { …

    } routing { get("/stock") { … } authenticate { put("/stock/{item}") { … } } } } Warehouse
  81. None
  82. fun Application.module() { install(FreeMarker) { templateLoader = ClassTemplateLoader( ) }

    Shop
  83. fun Application.module() { install(FreeMarker) { templateLoader = ClassTemplateLoader( ) }

    routing { get("/") { val items = warehouseService.getStock() call.respond(FreeMarkerContent( "index.ftl", mapOf("items" to items) ) }
 } Shop
  84. fun Application.module() { install(FreeMarker) { templateLoader = ClassTemplateLoader( ) }

    routing { get("/") { val items = warehouseService.getStock() call.respond(FreeMarkerContent( "index.ftl", mapOf("items" to items) ) }
 } Shop
  85. <div> <h3>Items</h3> <ul> <#list items as item> <li>${item.name} - ${item.price}</li>

    </#list> </ul> </div> Shop
  86. fun Application.module() { install(FreeMarker) { templateLoader = ClassTemplateLoader( ) }

    routing { get("/") { val items = warehouseService.getStock() call.respond(FreeMarkerContent( "index.ftl", mapOf("items" to items) ) }
 } Shop
  87. Shop - Warehouse Api suspend fun getStock(): List<Item> { return

    httpClient.get<List<StockDto ("http://localhost:8080/stock") }
  88. Shop - Warehouse Api suspend fun getStock(): List<Item> { return

    httpClient.get<List<StockDto ("http://localhost:8080/stock") }
  89. suspend fun getStock(): List<Item> { return httpClient.get<List<StockDto ("http://localhost:8080/stock") } val

    httpClient = HttpClient(OkHttp) { install(JsonFeature) { serializer = JacksonSerializer() } } Shop - Warehouse Api
  90. Shop - ShoppingCart & Orders Api suspend fun addToCart(item: Item)

    { httpClient.put<Unit>(shoppingCartUrl) { body = JsonContent(item) } }
  91. suspend fun addToCart(item: Item) { httpClient.put<Unit>(shoppingCartUrl) { body = JsonContent(item)

    } } Shop - ShoppingCart & Orders Api
  92. suspend fun addToCart(item: Item) { httpClient.put<Unit>(shoppingCartUrl) { body = JsonContent(item)

    } } private val shoppingCartUrl by lazy { application.environment.config .property("orders.shoppingcartUrl").getString() } Shop - ShoppingCart & Orders Api
  93. None
  94. Shopping Cart & Orders Service routing { put("/shoppingcart/items") { }

    }
  95. routing { put("/shoppingcart/items") { val session = call.getSession() val itemToAdd

    = call.receive<Item>() call.sessions.set( session.copy(shoppingCart = session.shoppingCart + itemToAdd) ) call.respond(Created, "Added ${itemToAdd.name} to cart.") } } Shopping Cart & Orders Service
  96. routing { put("/shoppingcart/items") { val session = call.sessions.get<UserSession>() val itemToAdd

    = call.receive<Item>() call.sessions.set( session.copy(shoppingCart = session.shoppingCart + itemToAdd) ) call.respond(Created, "Added ${itemToAdd.name} to cart.") } } Shopping Cart & Orders Service
  97. routing { put("/shoppingcart/items") { val session = call.sessions.get<UserSession>() val itemToAdd

    = call.receive<Item>() call.sessions.set( session.copy(shoppingCart = session.shoppingCart + itemToAdd) ) call.respond(Created, "Added ${itemToAdd.name} to cart.") } } Shopping Cart & Orders Service
  98. routing { put("/shoppingcart/items") { val session = call.sessions.get<UserSession>() val itemToAdd

    = call.receive<Item>() call.sessions.set( session.copy(shoppingCart = session.shoppingCart + itemToAdd) ) call.respond(Created, "Added ${itemToAdd.name} to cart.") } } ⚠ Sessions not RESTful Shopping Cart & Orders Service
  99. install(Sessions) { cookie<UserSession>("CART", storage = SessionStorageMemory()) { cookie.path = "/"

    serializer = jacksonSessionSerializer() } } Shopping Cart & Orders Service
  100. install(Sessions) { cookie<UserSession>("CART", storage = SessionStorageMemory()) { cookie.path = "/"

    serializer = jacksonSessionSerializer() } } data class UserSession(val name: String, val shoppingCart: List<Item>) Shopping Cart & Orders Service
  101. install(Sessions) { cookie<UserSession>("CART", storage = SessionStorageMemory()) { cookie.path = "/"

    serializer = jacksonSessionSerializer() } } data class UserSession(val name: String, val shoppingCart: List<Item>) Shopping Cart & Orders Service
  102. install(Sessions) { cookie<UserSession>("CART", storage = SessionStorageMemory()) { cookie.path = "/"

    serializer = jacksonSessionSerializer() } } data class UserSession(val name: String, val shoppingCart: List<Item>) toString() hashCode() equals() copy() Shopping Cart & Orders Service
  103. Microservices with Ktor - a thing?

  104. Ktor Not covered • Resilience • Monitoring & Metrics •

    Testing • Real Security
  105. Ktor Not covered • Resilience • Monitoring & Metrics •

    Testing • Real Security • Under heavy development • Documentation needs improvement • Community is very helpful
  106. Ktor Not covered • Resilience • Monitoring & Metrics •

    Testing • Real Security • Under heavy development • Documentation needs improvement • Community is very helpful https://ktor.io/quickstart/ http://slack.kotlinlang.org/ https://kotlinlang.org/
  107. Lovis Möller @lovisbrot lovis.moeller@codecentric.de meetup.com/Kotlin-User-Group-Hamburg Images from https://unsplash.com/