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. what’s the plan? • identify a problem with adapters •

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

    access /endpoint4 API Adapter a typical http microservice /endpoint2 /endpoint1 /endpoint3 /endpoint4
  5. 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
  6. /endpoint2 /endpoint1 /endpoint3 Business logic API Adapter API Adapter Data

    access /endpoint4 API Adapter a typical http microservice API Adapter API Adapter API Adapter
  7. 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…
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. @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
  14. @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
  15. 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
  16. 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
  17. 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
  18. summary • simple, modular adapters • testable and composable •

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

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