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

  2. HELLO, IT’S DAVID TRAINER GDE

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

    can we modularise, customise, test? • leverage Kotlin language features
  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
  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"))
  6. /endpoint2 /endpoint1 /endpoint3 Business logic API Adapter API Adapter Data

    access /endpoint4 API Adapter a typical http microservice /endpoint2 /endpoint1 /endpoint3 /endpoint4
  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
  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
  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…
  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
  11. introducing connect • modularise monolithic adapters • simple pattern •

    testable & composable
  12. action Action HTTP Req HTTP Resp Domain Req Domain Resp

    Action
  13. interface GitHubApiAction<R> { fun toRequest(): Request fun fromResponse(response: Response): R

    } data class GetUser(val username: String) : GitHubApiAction<UserDetails> { 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<String>) declaring actions
  14. adapter Adapter Domain Response Action http client Adapter

  15. interface GitHubApi { operator fun <R : Any> invoke(action: GitHubApiAction<R>):

    R companion object } define the adapter
  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 <R : Any> invoke(action: GitHubApiAction<R>) = action.fromResponse(http(action.toRequest())) } val gitHub: GitHubApi = GitHubApi.Http(OkHttp()) val user: UserDetails = gitHub(GetUser("octocat")) reimplementing the adapter
  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
  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
  19. testing the connect pattern

  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
  21. @Test fun `get user details`() { val githubApi = mockk<GitHubApi>()

    val userDetails = UserDetails("bob", listOf("http4k")) every { githubApi(any<GetUser>()) } returns userDetails assertThat(githubApi.getUser("bob"), equalTo(userDetails)) } mock the adapter
  22. class StubGitHubApi(private val users: Map<String, UserDetails>) : GitHubApi { override

    fun <R : Any> invoke(action: GitHubApiAction<R>): 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<String, UserDetails>) = users[action.username] private fun getRepoLatestCommit(action: GetRepoLatestCommit) = Commit(action.owner) stub the adapter
  23. class RecordingGitHubApi(private val delegate: GitHubApi) : GitHubApi { val recorded

    = mutableListOf<GitHubApiAction<*>>() override fun <R : Any> invoke(action: GitHubApiAction<R>): R { recorded += action return delegate(action) } } decorate the adapter
  24. vary your programming model

  25. interface GitHubApiAction<R> { fun toRequest(): Request fun fromResponse(response: Response): Result<R,

    Exception> } data class GetUser(val username: String) : GitHubApiAction<UserDetails> { 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
  26. summary • simple, modular adapters • testable and composable •

    flexible on model • works with any req/resp protocol
  27. • featherweight Kotlin-API adapters • zero reflection —> serverless &

    graal • in-memory fakes / storage http4k/http4k-connect
  28. THANKS! @daviddenton [email protected] https://dentondav.id http4k/http4k http4k/http4k-connect