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

Smash your adapter monolith with the Connect pattern

Smash your adapter monolith with the Connect pattern

Server-side code is regularly broken down into manageable chunks, based around endpoints. As a result it tends to be nicely factored. But over several projects we noticed that the same was not true of adapters that talk to 3rd party systems. These pieces of code tended to grow uncontrolled, and were not given the same level of attention.

In this talk, I'll be covering a pattern that we discovered to help break down your Adapter Monolith into modular, easily digestible and (most importantly) testable pieces.

This talk is based in Kotlin and takes advantage of language features such as Data Classes, Companion Objects, Operator Overloading and Extension Functions. However the pattern concepts themselves are applicable to any technology choice or programming model.

David Denton

March 19, 2021
Tweet

More Decks by David Denton

Other Decks in Programming

Transcript

  1. SMASH YOUR


    ADAPTER


    MONOLITH


    WITH THE


    CONNECT


    PATTERN
    David


    Denton

    View Slide

  2. HELLO, IT’S DAVID
    TRAINER
    GDE

    View Slide

  3. what’s the plan?
    • identify a problem with adapters


    • can we modularise, customise, test?


    • leverage Kotlin language features

    View Slide

  4. … but first, some http4k basics
    • server as a function


    • heavily TDD-driven


    • lightweight (core = 1mb)


    • run in-memory, or on 10 servers, 6 serverless platforms

    View Slide

  5. http4k basics (ii)
    typealias HttpHandler = (Request) -> Response


    fun interface Filter : (HttpHandler) -> HttpHandler


    val http = { req: Request -> Response(OK).body("hello") }


    val filter = Filter { next ->


    { req -> next(req.header("foo", “bar")) }


    }


    val decoratedHttp: HttpHandler = filter.then(filter).then(http)


    val response: Response = decoratedHttp(Request(GET, "/uri"))


    View Slide

  6. /endpoint2
    /endpoint1
    /endpoint3
    Business


    logic
    API


    Adapter
    API


    Adapter
    Data access


    /endpoint4
    API


    Adapter
    a typical http microservice
    /endpoint2
    /endpoint1
    /endpoint3
    /endpoint4

    View Slide

  7. fun MySecureApp(): HttpHandler =


    BearerAuth("my-very-secure-and-secret-bearer-token")


    .then(


    routes(


    echo(),


    health()


    )


    )


    fun echo() = "/echo" bind


    POST to { req: Request -> Response(OK).body(req.bodyString()) }


    fun health() = "/health" bind


    GET to { req: Request -> Response(OK).body("alive!") }


    val server = MySecureApp().asServer(Netty(8080)).start()
    server-side

    View Slide

  8. /endpoint2
    /endpoint1
    /endpoint3
    Business


    logic
    API


    Adapter
    API


    Adapter
    Data access


    /endpoint4
    API


    Adapter
    a typical http microservice
    API


    Adapter
    API


    Adapter
    API


    Adapter

    View Slide

  9. class GitHubApi(client: HttpHandler) {


    private val http = SetBaseUriFrom(Uri.of("https://api.github.com"))


    .then(SetHeader("Accept", "application/vnd.github.v3+json"))


    .then(client)


    fun getUser(username: String): UserDetails {


    val response = http(Request(GET, "/users/$username"))


    return UserDetails(userNameFrom(response), userOrgsFrom(response))


    }


    fun getRepoLatestCommit(owner: String, repo: String) = ....


    }


    val gitHub: GitHubApi = GitHubApi(OkHttp())


    val user: UserDetails = gitHub.getUser("octocat")
    the adapter monolith…

    View Slide

  10. a real world example: K8S API
    • OpenApi Spec


    • 833 endpoints


    • 87k lines JSON


    • Kotlin version**


    • 32k LOC -> 17mb JAR
    * https://toolbox.http4k.org ** http://github.com/daviddenton/http4k-k8s-api

    View Slide

  11. introducing connect
    • modularise monolithic adapters


    • simple pattern


    • testable & composable

    View Slide

  12. action
    Action
    HTTP Req
    HTTP Resp
    Domain Req
    Domain Resp
    Action

    View Slide

  13. interface GitHubApiAction {


    fun toRequest(): Request


    fun fromResponse(response: Response): R


    }


    data class GetUser(val username: String) : GitHubApiAction {


    override fun toRequest() = Request(GET, “/users/$username")


    override fun fromResponse(response: Response) =


    UserDetails(userNameFrom(response), userOrgsFrom(response))


    }


    data class UserDetails(val name: String, val orgs: List)
    declaring actions

    View Slide

  14. adapter
    Adapter
    Domain
    Response
    Action
    http client
    Adapter

    View Slide

  15. interface GitHubApi {


    operator fun invoke(action: GitHubApiAction): R


    companion object


    }
    define the adapter

    View Slide

  16. fun GitHubApi.Companion.Http(client: HttpHandler) = object : GitHubApi {


    private val http = SetBaseUriFrom(Uri.of(“https://api.github.com"))


    .then(SetHeader("Accept", "application/vnd.github.v3+json"))


    .then(client)


    override fun invoke(action: GitHubApiAction) =


    action.fromResponse(http(action.toRequest()))


    }


    val gitHub: GitHubApi = GitHubApi.Http(OkHttp())


    val user: UserDetails = gitHub(GetUser("octocat"))
    reimplementing the adapter

    View Slide

  17. fun GitHubApi.getUser(username: String): UserDetails


    = invoke(GetUser(username))


    fun GitHubApi.getLatestRepoCommit(owner: String, repo: String): Commit


    = invoke(GetRepoLatestCommit(owner, repo))


    val user: UserDetails = gitHub.getUser(“octocat")
    recreating our api

    View Slide

  18. fun GitHubApi.getLatestUser(org: String, repo: String): UserDetails {


    val commit = getLatestRepoCommit(org, repo)


    return getUser(commit.author)


    }


    val latestUser: UserDetails = gitHub.getLatestUser("http4k", "http4k-connect")
    custom composite actions

    View Slide

  19. testing the


    connect pattern

    View Slide

  20. @Test
    fun `translates request`() {
    assertThat(GetUser("foobar").toRequest(), equalTo(Request(GET, "/users/foobar")))
    }
    @Test
    fun `translates response`() {
    assertThat(GetUser("foobar").fromResponse(Response(OK).body("foobar/admin,mgmt")),
    equalTo(UserDetails("foobar", listOf("admin", "mgmt"))))
    }
    testing actions

    View Slide

  21. @Test


    fun `get user details`() {


    val githubApi = mockk()


    val userDetails = UserDetails("bob", listOf("http4k"))


    every { githubApi(any()) } returns userDetails


    assertThat(githubApi.getUser("bob"), equalTo(userDetails))


    }


    mock the adapter

    View Slide

  22. class StubGitHubApi(private val users: Map) : GitHubApi {


    override fun invoke(action: GitHubApiAction): R =


    when (action) {


    is GetUser -> getUser(action, users) as R


    is GetRepoLatestCommit -> getRepoLatestCommit(action) as R


    else -> throw UnsupportedOperationException()


    }


    }


    private fun getUser(action: GetUser, users: Map) = users[action.username]
    private fun getRepoLatestCommit(action: GetRepoLatestCommit) = Commit(action.owner)
    stub the adapter

    View Slide

  23. class RecordingGitHubApi(private val delegate: GitHubApi) : GitHubApi {


    val recorded = mutableListOf>()


    override fun invoke(action: GitHubApiAction): R {


    recorded += action


    return delegate(action)


    }


    }
    decorate the adapter

    View Slide

  24. vary your


    programming model

    View Slide

  25. interface GitHubApiAction {


    fun toRequest(): Request


    fun fromResponse(response: Response): Result


    }


    data class GetUser(val username: String) : GitHubApiAction {


    override fun toRequest() = Request(GET, "/users/$username")


    override fun fromResponse(response: Response) =


    when {


    response.status.successful -> Success(UserDetails(


    userNameFrom(response),


    userOrgsFrom(response))


    )


    else -> Failure(RuntimeException("Status: " + response.status))


    }


    }
    result4k

    View Slide

  26. summary
    • simple, modular adapters


    • testable and composable


    • flexible on model


    • works with any req/resp protocol

    View Slide

  27. • featherweight Kotlin-API adapters


    • zero reflection —> serverless & graal


    • in-memory fakes / storage
    http4k/http4k-connect

    View Slide

  28. THANKS!
    @daviddenton [email protected]
    https://dentondav.id
    http4k/http4k http4k/http4k-connect

    View Slide