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

Microservices with Kotlin and Ktor

Lovis
April 15, 2019

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.

Lovis

April 15, 2019
Tweet

More Decks by Lovis

Other Decks in Programming

Transcript

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

    with multiple small teams. Not cheap.
  2. • Consice, Safe, Pragmatic and tool-friendly • Created by JetBrains

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

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

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

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

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

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

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

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

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

    Application module ).start(wait = true) }
  12. fun String.shuffle(): String { return this.toCharArray() .toList() .shuffled() .joinToString("") }

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

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

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

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

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

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

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

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

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

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

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

    { call.respond(db.stock.map {(name,quantity)  StockDto(name, quantity) }) } } } Warehouse
  24. fun Application.module() { install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } }

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

     routing { get("/stock") { call.respond(warehouse.stock.map { (k, v)  StockDto(k, v) }) }  Warehouse
  26. 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
  27. get(" ") { } post(" ") { } put(" ")

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

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

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

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

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

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

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

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

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

    128).uniqueIndex() val quantity = integer("quantity") }  insert { column -> column[itemName] = "Fish" column[quantity] = 100 } Warehouse
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. fun Application.module() { install(ContentNegotiation) { … } install(Authentication) { basic

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

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

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

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

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

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

    httpClient.get<List<StockDto ("http://localhost:8080/stock") }
  50. 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
  51. Shop - ShoppingCart & Orders Api suspend fun addToCart(item: Item)

    { httpClient.put<Unit>(shoppingCartUrl) { body = JsonContent(item) } }
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. install(Sessions) { cookie<UserSession>("CART", storage = SessionStorageMemory()) { cookie.path = "/"

    serializer = jacksonSessionSerializer() } } Shopping Cart & Orders Service
  58. 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
  59. 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
  60. 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
  61. Ktor Not covered • Resilience • Monitoring & Metrics •

    Testing • Real Security • Under heavy development • Documentation needs improvement • Community is very helpful
  62. 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/