Week of Java: Part 4 - A Multi-layer Core for Your Function

In previous parts, we have covered the basics to set up our serverless project, use a development environment using emulation software, and a general multi-tier architecture for our project. In this part, I’ll present and implement design patterns to develop a serverless architecture following a Function as a Microservice (FaaM) style.

Service Granularity

In the mid-90s, the web was starting a historical revolution and worldwide systems needed a way to integrate with each other. During the 70s, we saw some theoretical solutions to integrate remote systems and in the 80s some of them were implemented. However, popular solutions such as Remote Procedure Calls (RPC), CORBA and Distributed Computing Environment (DCE) were not ready for a global scale change and, most of the time, their implementations were not agnostic enough to work in a hybrid environment. Then the world met a new way to integrate their systems: Service Oriented Architecture (SOA).

From its humble beginnings in 1996 to a complete revolution using SOAP in the 2000s, SOA became the software design and implementation that changed the tech landscape forever. Evolving into a new mindset during the 2010s with REST, Event Driven Architectures (EDA) and Microservices, the question that any developer, architect and manager asks is: How big should a service be?

The classic SOA presented two main concepts: Coarse-grained (CA) and Fine-grained (FA). In the past, a CA was defined as a service with a broad business scope, while FA was considered a service that focused on a narrow scope. However, while a huge CA may produce a monster with a lot of business capabilities, a FA service risks creating hundreds or thousands of mini problems. In the end, both extremes can reduce the modifiability, scalability and security of the solution.

For that reason, I like the granularity principle that the Service-Oriented Framework (SOMF) provides. In this framework, a service can be divided into three different kinds: Cluster, Composite and Atomic, which can be defined as:

Atomic: A fine-grained service that is impractical to decompose because of its suggested limited capabilities or processes

Composite: A coarse-grained service comprised of internal fine-grained atomic or composite services, forming hierarchical parent-child associations

Cluster: An association of services grouped by related business or technical processes that collaborate to offer solutions

— SOMF 2.1 Specifications © 2008–2011

But, how can we apply those concepts to our serverless project? Let’s first understand how Serverless Framework + AWS Lambda integrate some similar concepts.

Serverless Granularity

When we’re working with Serverless Framework + AWS Lambda we need to take into account three main concepts for any project:

  • Event: Anything that can trigger a function (HTTP calls, S3 upload, Cloudwatch alarm, etc).
  • Function: A piece of code that handles an event and executes program instructions that perform a specific task.
  • Service: Where you define your AWS Lambda Functions, the events that trigger them and any AWS infrastructure resources they require.

According to Serverless Framework, we should create an AWS lambda function per single CRUD operation. Meaning that an entity User will become a service with 4 AWS lambda functions. However, if we follow this practice, just having 10 entities will result in 40 functions! So now you need to manage 40 lambda functions, one for every single event. Also, let’s remember that our entry point is going to be the AWS API Gateway and when using services, it creates a different API Gateway for each service.

Function as a Microservice (FaaM)

For that reason, I like to match the framework concepts with the ones defined by SOMF. With that in mind, I propose a concept called Function as a Microservice (FaaM) that I defined as:

A design pattern where each lambda function behaves as a small cluster with meaningful business composite capabilities, that receives, handles, routes and processes multiple atomic events.

Using this pattern instead of having a function to “add a new city”, we have a geolocation function that knows how to CRUD countries, cities and regions. These small microservice functions shouldn’t manage more than 7± 2 entities (following Miller’s Law), otherwise you’re going to have a huge monolith in a function. Note: Keep in mind that this pattern assumes that every single function is responsible for its own data source too.

But how can we implement a FaaM using AWS Lambda? Well, the answer is using good routing patterns!

Request-Dispatcher

In AWS every single Function should have a unique handler to resolve the incoming events. Using the Serverless Framework we can define the handler file that each function is going to use. By default, Serverless creates a Handler.kt file like this:

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import org.apache.logging.log4j.LogManager

class Handler:RequestHandler<Map<String, Any>, ApiGatewayResponse> {
  override fun handleRequest(input:Map<String, Any>, context:Context):ApiGatewayResponse {
    LOG.info("received: " + input.keys.toString())

    return ApiGatewayResponse.build {
      statusCode = 200
      objectBody = HelloResponse("Go Serverless v1.x! Your Kotlin function executed successfully!", input)
      headers = mapOf("X-Powered-By" to "AWS Lambda & serverless")
    }
  }

  companion object {
    private val LOG = LogManager.getLogger(Handler::class.java)
  }
}

Instead of copying and pasting the same logic, we can always reuse the generic and abstract code (essentially it’s always the same thing). We just need to know how to route to different logic depending on the event type. For those cases, we can use a Client-Dispatcher-Server Pattern.

In our specific case, this routing will depend on the URI that a client uses to make a request. Meaning that if we make an HTTP request to /movie, we should be capable of routing that request to a specific Kotlin function based on its request URI. The next example presents a routing structure using the same handler:

service: MyBlockbuster
...

functions:
  users:
    handler: com.myblockbuster.Handler
    events:
      - http:
          path: login
          method: post
      - http:
          path: logout
          method: post
      - http:
          path: signup
          method: post

  geolocation:
    handler: com.myblockbuster.Handler
    events:
      - http:
          path: location/country
          method: get
      - http:
          path: location/country/{code}
          method: get
      - http:
          path: location/region
          method: get
      - http:
          path: location/region/{code}
          method: get
      - http:
          path: location/city
          method: get
      - http:
          path: location/city/{code}
          method: get
  posts:
    handler: com.myblockbuster.Handler
    events:
      - http:
          path: movie
          method: get
      - http:
          path: movie/{id}
          method: get
      - http:
          path: movie
          method: post
      - http:
          path: movie
          method: put
      - http:
          path: movie/{id}
          method: delete
          

But using the same handler, how can we route properly? Well, I created a different yaml file using regex that knows how to route to a specific class and function depending on the URI. In the resources folder I created a routes.yml file like this:

routes:
  # =========================
  # Movies Microservice
  # =========================

  - regex: '^/movie(/*+)?$'
    cls: com.myblockbuster.movies.controllers.MovieController
    func: movie

  # =========================
  # User Microservice
  # =========================

  - regex: '^/login(/)?$'
    cls: com.myblockbuster.users.controllers.UserController
    func: login
  - regex: '^/logout(/)?$'
    cls: com.myblockbuster.users.controllers.UserController
    func: logout
  - regex: '^/signup(/)?$'
    cls: com.myblockbuster.users.controllers.UserController
    func: signup

  # =========================
  # Geolocation Microservice
  # =========================

  - regex: '^/location/country(/*+)?$'
    cls: com.myblockbuster.geolocation.controllers.LocationController
    func: country
  - regex: '^/location/region(/*+)?$'
    cls: com.myblockbuster.geolocation.controllers.LocationController
    func: region
  - regex: '^/location/city(/*+)?$'
    cls: com.myblockbuster.geolocation.controllers.LocationController
    func: region

If we take a look to the User Microservice, the first element of the list means that when there’s a URI that satisfies the regex ^/login(/)?$ it will execute the function login in the class UserController of the package com.myblockbuster.users.controllers. 

Note: If you have worked with frameworks like Django or Ruby on Rails you should be familiar with this routing.

Nevertheless, this is just the static way that we want to route. Now we need to implement the piece of code for that.

Gradle file

Include the next dependencies to your project:

// Updates the Kotlin Gradle Plugin
dependencies {
   classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.21'
   ...
}// Updates the Kotlin Gradle Stdlib
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: '1.2.21'// Add new dependencies
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.1.2'
compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: '1.1.1'

Data models

I created the next models.kt file to support all the entities that my new architecture requires.

package com.myblockbuster.core

import com.amazonaws.services.lambda.runtime.Context
import java.io.Serializable

/**
 * Generic Model that any body in the future should reply to
 */
interface Model: Serializable {
    var id: Int?
}

/**
 * In case of no model this is the default model reply
 */
class EmptyModel(override var id: Int? = null) : Model

/**
 * In case of error this is the Default Error model to return
 */
class ErrorModel(var message: String): Model {
    override var id: Int?
        get() = null
        set(value) {}

    constructor(): this("")
}

/**
 * Represents a client Request
 * @property input JSON object with all the request data described in https://serverless.com/framework/docs/providers/aws/events/apigateway/
 * @property context Lambda function Metadata
 */
interface Request {
    var input: Map<String, Any>
    var context: Context
}

/**
 * Request Dispatcher route to a concrete function based on a regex
 * @property regex Regex that needs to satisfy to route the request
 * @property func Function name to be executed
 * @property cls ClassPath
 * @constructor Creates an empty object with empty values
 */
data class Route(var regex: String, var func: String, var cls: String) {
    constructor(): this("", "", "")
}

/**
 * List of routes
 * @property List of routes
 * @constructor emptyList
 */
data class Routes(var routes: List<Route>) {
    constructor(): this(emptyList())
}

/**
 * Represents an APIGateway Request (implements {@link Request})
 * @property input JSON object with all the request data described in https://serverless.com/framework/docs/providers/aws/events/apigateway/
 * @property context Lambda function Metadata
 */
class ApiGatewayRequest(override var input:Map<String, Any>, override var context: Context): Request

Exceptions

As usual, it’s always a good practice to manage your own exceptions. In my case, I’m also defining the HTTP code that a specific exception should raise. The exceptions.kt file looks like:

package com.myblockbuster.core

/**
 * Custom exception for our project
 * @property code HTTP code that the client is going to receive
 * @property message Exception message
 * @constructor By default it will reply with a 500 HTTP method
 */
abstract class MyException(var code: Int, override var message: String): Exception(message) {
    constructor() : this(500, "Internal MyBlockbuster Exception")
}

/**
 * Exception thrown when the body doesn't contain all the required fields
 * @property body Input body
 */
class InvalidArguments(private var body: String) :
        MyException(400, "The entity $body doesn't contain all the required fields")

/**
 * Router Exception (request-dispatcher)
 * @property resource The route/resource that failed
 * @constructor The default exception is 404 not found exception
 */
class RouterException(private var resource: String) :
        MyException(404, "The route/resource $resource doesn't exist")

Dispatcher

The next interface presents the basic capabilities that any Dispatcher should provide. Even when we are just going to have one dispatcher in this project, it is always a good practice to isolate implementations from the functional contracts.

package com.myblockbuster.core.dispatchers

/**
 * Dispatcher on charge of routing our client request to the correct function
 * @param K Key type
 * @param T Return object type
 */
interface Dispatcher<in K, out T> {

    /**
     * Function that finds and executes a specific function given a {@link K} unique key
     * returning a {@link T} result object
     * @param key Unique key to find and execute a function
     * @return T
     */
    fun locate(key: K): T?
}

Based on the previous interface we can have the next RequestDispatcher implementation.

package com.myblockbuster.core.dispatchers

import java.io.File
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.myblockbuster.core.ApiGatewayRequest
import com.myblockbuster.core.RouterException
import com.myblockbuster.core.Routes
import kotlin.reflect.full.createInstance

/**
 * Request Dispatcher implementation
 */
open class RequestDispatcher: Dispatcher<ApiGatewayRequest, Any> {

    @Throws(RouterException::class)
    override fun locate(key: ApiGatewayRequest): Any? {
        val path = key.input["path"]

        var response: Any? = null
        var found: Boolean = false

        for ((regex, function, cls) in ROUTER.routes) {
            val match = Regex(regex).matchEntire(path as CharSequence)

            if (match != null) {
                // Finds the class based on the absolute class name and runs the function
                val kClass = Class.forName(cls).kotlin
                val func = kClass.members.find { it.name == function }
                response = func?.call(kClass.createInstance(), key)
                found = true

                break
            }
        }

        if (!found)
            throw RouterException(path as? String ?: "")

        return response
    }

    /**
     * Singleton that loads the routes once and keep them on memory
     */
    companion object BackendRouter {
        private val FILE = File(javaClass.classLoader.getResource("routes.yml")!!.file)
        val ROUTER: Routes = ObjectMapper(YAMLFactory()).readValue(FILE, Routes::class.java)
    }

}

This dispatcher implementation executes a Kotlin function and returns the result object to our client. To achieve that, I’m using the default Kotlin reflection methods, so you need to be cautious when using it. After executing some tests, this usage of reflection is not affecting performance too much because I’m using the complete Classpath. But if you try more dynamic code, performance can change from milliseconds to seconds.

Response

By default, Serverless creates a Response class that I changed into an interface. Just like with the Dispatcher, the main reason for that it’s just to keep implementation and functional contracts isolated from each other. Note: After this, you can also delete the HelloResponse class.

package com.myblockbuster

interface Response

ApiGatewayResponse

Similar to the class Response, Serverless also creates an ApiGatewayResponse class. To accept the new changes for the Client-Dispatcher-Server, let’s use the next piece of code:

package com.myblockbuster

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.myblockbuster.core.Model
import org.apache.logging.log4j.LogManager
import java.nio.charset.StandardCharsets
import java.util.*

/**
 * Instead of having a generic response for everything now the Response class is an interface
 * and we create a specific implementation of it
 */
class ApiGatewayResponse(
        val statusCode: Int = 200,
        var body: String? = null,
        val headers: Map<String, String>? = Collections.emptyMap(),
        val isBase64Encoded: Boolean = false
): Response {

  companion object {
    inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
    val LOG = LogManager.getLogger() // Singleton Logger
  }

  /**
   * Uses the Builder pattern to create the response
   */
  class Builder {
    var objectMapper: ObjectMapper = ObjectMapper()
    var statusCode: Int = 200
    var rawBody: String? = null
    var headers: Map<String, String>? = Collections.emptyMap()
    var objectBody: Model? = null
    var listBody: List<Any>? = null
    var binaryBody: ByteArray? = null
    var base64Encoded: Boolean = false

    fun build(): ApiGatewayResponse {
      var body: String? = null

      when {
        rawBody != null -> body = rawBody as String
        objectBody != null || (listBody != null && listBody!!.isNotEmpty()) -> try {
          body = when {
            objectBody != null -> objectMapper.writeValueAsString(objectBody)
            else -> objectMapper.writeValueAsString(listBody)
          }
        } catch (e: JsonProcessingException) {
          LOG.error("Failed to serialize object", e)
          throw RuntimeException(e)
        }
        binaryBody != null -> body = String(Base64.getEncoder().encode(binaryBody), StandardCharsets.UTF_8)
      }
      return ApiGatewayResponse(statusCode, body, headers, base64Encoded)
    }
  }
}

New Handler

Now we can create the reusable handler that multiple functions will use for their routing. The Handler.kt file looks like:

package com.myblockbuster

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import com.myblockbuster.ApiGatewayResponse.Companion.LOG
import com.myblockbuster.core.*
import com.myblockbuster.core.dispatchers.RequestDispatcher

open class Handler: RequestHandler<Map<String, Any>, ApiGatewayResponse> {

  var requestDispatcher: RequestDispatcher = RequestDispatcher()

  override fun handleRequest(input:Map<String, Any>, context: Context): ApiGatewayResponse {

    var status = 500
    var body: Any? = EmptyModel()

    try {
      body = requestDispatcher.locate(ApiGatewayRequest(input, context)) ?: EmptyModel()

      status = when (body is EmptyModel) {
        true -> 204
        false -> 200
      }
    }
    catch (e: MyException) {
      LOG.error(e.message, e)
      status = e.code
      body = ErrorModel(e.message)
    }
    catch (e: Throwable) {
      LOG.error(e.message, e)
      status = 500
      body = ErrorModel("Internal server error")
    }
    finally {
      return ApiGatewayResponse.build {
        statusCode = status
        if (body is Model)
          objectBody = body as Model?
        else
          listBody = body as List<Any>
        headers = mapOf("X-Powered-By" to "AWS Lambda & Serverless")
      }
    }
  }
}

Controllers

Finally, let’s create a layer with our business logic. Following the same principle of an MVC app, we can create some controllers with the necessary logic.

package com.myblockbuster.core.controllers

import com.fasterxml.jackson.databind.ObjectMapper
import com.myblockbuster.core.InvalidArguments
import com.myblockbuster.core.Model
import com.myblockbuster.core.Request

/**
 * Controller that receives a request and reply with a message of type {@link M}
 * @param M Model type
 */
interface Controller<M> {

    fun anyToInt(value: Any, valueType: String = "page"): Int {
        return when (value) {
            is String -> value.toIntOrNull() ?: throw Exception("$valueType must be a number")
            is Int -> value
            else -> throw Exception("$valueType must be a number")
        }
    }

    fun getStringAnyMap(request: Request, key: String): Map<String, Any> {
        return if (request.input.containsKey(key) && request.input[key] != null)
            request.input[key] as Map<String, Any>
        else
            emptyMap()
    }

    fun getResource(request: Request): String {
        return request.input["resource"] as String
    }

    fun getHeaders(request: Request) : Map<String, Any> {
        return getStringAnyMap(request, "headers")
    }

    fun getPathParameters(request: Request) : Map<String, Any> {
        return getStringAnyMap(request, "pathParameters")
    }

    fun getQueryStringParameters(request: Request) : Map<String, Any> {
        return getStringAnyMap(request, "queryStringParameters")
    }

    fun getRawBody(request: Request) : Any {
        if (request.input["body"] != null)
            return if (request.input["body"] is String) request.input["body"] as String else request.input["body"] as Map<String, Any>
        else
            throw InvalidArguments("body")
    }

    fun <T : Model> getEntity(rawBody: Any, cls: Class<T>): T {
        val objectMapper = ObjectMapper()

        return if (rawBody is String) objectMapper.readValue(rawBody, cls) else objectMapper.convertValue(rawBody, cls)
    }
}

The functions of this Interface/Mixin are some basic capabilities that you may need to process a request. However, most of the time the processing of our request will be quite repetitive. For that reason, I decided to create a Micro-HTTP router that is based on the HTTP method it routes to a specific Kotlin function.

Our Client-Dispatcher-Server implementation. Given that we are using reflection to find, create and execute a controller, there’s no need for a direct connection between the client (handler) and the server (controller)

Micro HTTP-Router

To create the Micro-Router we can create a generic Service interface that defines a basic CRUD for an object. But first, let’s define some additional models that we need.

Models

// You can use the same models.kt class

// ------------------------------------------------------
// HTTP and W3C compliant models for HATEOAS/HAL
// ------------------------------------------------------

/**
 * A link relation indicating what is the first, last, previous and next resource
 * This is a good practice for self discovery using HAL http://stateless.co/hal_specification.html
 * @property value Enum value
 */
enum class LinkRel(val value: String) {
    NEXT("next"),
    FIRST("first"),
    LAST("last"),
    SELF("self")
}

/**
 * Resource link
 * @property rel Type of link
 * @property url URL String
 */
data class Link(var rel: LinkRel, var url: URL)

/**
 * Metadata of a specific page, in case of HAL it's always good practice to return context data
 * @param limit Max amount of objects to show per page (like SQL LIMIT)
 * @param offset From which element we start to count
 * @param count Total elements
 * @property totalPages Total pages that it's basically the ceil(COUNT/LIMIT)
 * @property pageNumber Current page that it's basically the ceil(OFFSET+1/LIMIT)
 * @property totalElements It's equals to COUNT
 */
class PageMetadata(limit: Int, offset: Int, count: Int) {
    val totalPages: Int = ceil(count.toDouble() / limit.toDouble()).toInt()
    val pageNumber: Int = ceil((offset.toDouble() + 1) / limit.toDouble()).toInt()
    val totalElements: Int = count

    val first: Boolean
        get() = pageNumber == 1

    val last: Boolean
        get() = pageNumber == totalPages

    override fun toString(): String {
        return "PageMetadata(totalPages=$totalPages, pageNumber=$pageNumber, " +
                "totalElements=$totalElements, first=$first, last=$last)"
    }
}

/**
 * In case of multiple elements we reply with a page, that includes the data and metadata of a set of objects
 * @param sort Map of values to sort (based on the URL params, that means that we don't include ?page & ?size)
 * @param limit Max amount of objects to show per page (like SQL LIMIT)
 * @param offset From which element we start to count
 * @property content List of objects
 * @property links HAL links
 * @property sort Way to sort our results based on URL params
 * @property metadata HAL metadata
 */
class Page<T>(sort: Map<String, Any>, limit: Int, offset: Int, count: Int,
              var content: List<T>, var links: List<Link> = emptyList()): Model {

    override var id: Int?
        get() = null
        set(value) {}

    var sort: Map<String, Any> = sort
        get() = field.minus(arrayOf("page", "size"))

    val metadata: PageMetadata = PageMetadata(limit, offset, count)


    override fun toString(): String {
        return "Page(content=$content, sort=$sort, links=$links, metadata=$metadata)"
    }
}

/**
 * Desired pagination
 * @property page Page Number to present
 * @property size Size of the pages
 */
data class Pagination(var page: Int, var size: Int)

Service

This interface defines the basic CRUD capabilities that any entity should provide. This contract is based on frameworks like Loopback, Django RF, RoR and Spring. The Service.kt looks something like:

package com.myblockbuster.core.services

import com.myblockbuster.core.Page
import com.myblockbuster.core.Pagination

/**
 * Service that exposes the capabilities of a {@link T} element
 * @param <K> Natural Key type
 * @param <T> Element type
 * @param <F> Filter type
 * @param <U> User permissions
 */
interface Service<T, in U> {

    /**
     * Creates a [T] in the system
     * @param user [U] User who is requesting (to verify permissions)
     * @param element [T] that is going to be saved
     * @return element with the saved status
     */
    fun create(user: U, element: T): T

    /**
     * Find a set of [T] by a given set of optional parameters
     * @param user [U] User who is requesting (to verify permissions)
     * @param filters Optional parameters
     * @param pagination How to paginate the result
     * @return list of [T]
     */
    fun findAll(user: U, filters: Map<String, Any> = mapOf( "order" to "creationDate" ),
                pagination: Pagination = Pagination(0, 50)): Page<T>

    /**
     * Finds one [T] by the unique ID
     * @param user [U] User who is requesting (to verify permissions)
     * @param id Unique id
     * @return [T] that has that ID
     */
    fun findOne(user: U, id: Int): T

    /**
     * Finds one [T] by a unique natural [K] key
     * @param user [U] User who is requesting (to verify permissions)
     * @param filters Set of filters that will return a unique value
     * @return [T] that has that [Any] natural key
     */
    fun findOne(user: U, filters: Map<String, Any> = emptyMap()): T

    /**
     * Updates a [T]
     * @param user [U] User who is requesting (to verify permissions)
     * @param element [T] that is going to be updated
     * @return updated element
     */
    fun update(user: U, element: T): T

    /**
     * Deletes a [T] given a unique ID
     * @param user [U] User who is requesting (to verify permissions)
     * @param id Unique ID of the [T]
     */
    fun delete(user: U, id: Int)

    /**
     * Returns the amount or [T] entities in the system
     * @param user [U] User who is requesting (to verify permissions)
     * @param filters Set of filters
     * @return list of [T]
     */
    fun count(user: U, filters: Map<String, Any> = emptyMap()): Int

    /**
     * Verifies if a entity with a specific ID exists
     * @param user [U] User who is requesting (to verify permissions)
     * @param id Unique id
     * @return [Boolean] value indicating true if exists or false if not
     */
    fun exists(user: U, id: Int): Boolean
}

In this Service I’m assuming that we’ll receive a User to validate permissions to execute that function. We can use an AnonymousUser class to define a user who hasn’t logged into the system. The next models.kt file exposes an example of that.

User Models

// You can use the same models.kt class

// ------------------------------------------------------
// User management
// ------------------------------------------------------

/**
 * It's a way to keep a canonical Datetime serializer
 */
class DateTimeSerializer @JvmOverloads constructor(t: Class<DateTime>? = null) : StdSerializer<DateTime>(t) {

    @Throws(IOException::class, JsonProcessingException::class)
    override fun serialize(value: DateTime, gen: JsonGenerator, arg2: SerializerProvider) {
        gen.writeString(formatter.print(value))
    }

    private val formatter = ISODateTimeFormat.basicDateTime()
}

/**
 * A base model with some minimum attributes that any entity in the system should have
 * @property id Unique ID of that element
 * @property createdAt When it was created
 * @property updatedAt Last time that the entity changed
 */
abstract class BaseModel(override var id: Int?): Model {
    constructor(): this(0)

    @JsonSerialize(using = DateTimeSerializer::class)
    var createdAt: DateTime? = DateTime()

    @JsonSerialize(using = DateTimeSerializer::class)
    var updatedAt: DateTime? = DateTime()
}

/**
 * Basic fields that a User needs
 * @property email User email
 */
abstract class User: BaseModel() {
    open var email: String = ""
}

/**
 * Anonymous User -> User who is not logged into the system
 */
class AnonymousUser: User()

HTTP Router

The easiest and most reusable way that I have found to develop the router, was using the Controller interface/mixin that we already defined. You can always create another interface called HttpRouter and implement it in the Controller interface if needed.

// You can add this code to the Controller.kt class

companion object {
        // HTTP constants
        const val HTTP_METHOD: String = "httpMethod"
        const val HTTP_GET: String = "get"
        const val HTTP_POST: String = "post"
        const val HTTP_PUT: String = "put"
        const val HTTP_DELETE: String = "delete"
        const val HTTP_PATCH: String = "patch"

        // Pagination constants
        const val LIMIT: Int = 50
        const val MAX_LIMIT: Int = 100
        const val OFFSET: Int = 0
        const val MIN_LIMIT: Int = 0
}


  fun getPagination(queryParameters: Map<String, Any>): Pagination {
      var page: Int = ceil(OFFSET.toDouble() / LIMIT).toInt()
      var size: Int = LIMIT
      val pagination = Pagination(page, size)

      if (queryParameters.containsKey("page")) {
          page = anyToInt(queryParameters["page"]!!)
          pagination.page = if (page >= MIN_LIMIT + 1) page - 1 else MIN_LIMIT
      }

      if (queryParameters.containsKey("size")) {
          size = anyToInt(queryParameters["size"]!!, "size")
          pagination.size = if (size < MAX_LIMIT) size else MAX_LIMIT
      }

      return pagination

  }

  /**
   * Http router, it receives a class type to return, a request and service to automatically execute and return a response
   * @param cls Class return type
   * @param request Http Client request
   * @param service CRUD service to execute
   */
  fun <T : Model> defaultRouting(cls: Class<T>, request: Request, service: Service<T, User>): Any {
      val resource: String = getResource(request)
      val headers: Map<String, Any> = getHeaders(request)
      val pathParameters: Map<String, Any> = getPathParameters(request)
      val queryParameters: Map<String, Any> = getQueryStringParameters(request)

      return when((request.input[HTTP_METHOD] as String).toLowerCase()) {
          HTTP_GET -> {
              when {
                  resource.endsWith("findOne", true) -> service.findOne(AnonymousUser(), queryParameters)
                  pathParameters.containsKey("id") -> {
                      val id = pathParameters["id"]

                      when (id) {
                          is Int -> service.findOne(AnonymousUser(), id)
                          else -> throw Exception("Id must be an integer")
                      }
                  }
                  else -> {
                      return service.findAll(AnonymousUser(), queryParameters, getPagination(queryParameters))
                  }
              }
          }
          HTTP_POST -> {
              service.create(AnonymousUser(), getEntity(getRawBody(request), cls))
          }
          HTTP_PUT -> {
              service.update(AnonymousUser(), getEntity(getRawBody(request), cls))
          }
          HTTP_DELETE -> {
              service.delete(AnonymousUser(), getEntity(getRawBody(request), cls).id!!)
          }
          HTTP_PATCH -> {
              service.update(AnonymousUser(), getEntity(getRawBody(request), cls))
          }

          else -> {
              throw Exception()
          }
      }
}

The previous code fragments show how to route to a specific method according to the HTTP method received as a parameter. With this piece of code, you just need to implement each CRUD method or throw a MyException object with the code 405 (method not allowed).

Implementation Example

The controller can be defined according to your needs, that’s why the mixin doesn’t have any contract to follow (the only constraint is that each function needs to receive a Request object as argument). The next code fragments are examples that you can reuse for your own project.

Movies Exceptions

// File exceptions.kt in the movies package (microservice)
package com.myblockbuster.movies

import com.myblockbuster.core.MyException

class MovieAlreadyExistsException(code: Int = 400, message: String = "Movie Already Exists") : MyException(code, message)

class MovieNotExistsException(code: Int = 404, message: String = "Movie Doesn't Exists") : MyException(code, message)

Movies Models

// File models.kt in the movies package (microservice)
package com.myblockbuster.movies

import com.myblockbuster.core.BaseModel

data class Person(var name: String, var surname: String) {
    constructor() : this("", "")
}

data class Movie(var title: String, var rate: Double, var language: String, var director: Person,
                 var cast: List<Person>, var code: String, override var id: Int?): BaseModel() {
    constructor(): this("", 0.0, "EN", Person(), listOf(Person()), "", -1)
}

Movie Service Implementation (mock without Security and Persistence layer)

package com.myblockbuster.movies.services

import com.myblockbuster.core.Page
import com.myblockbuster.core.Pagination
import com.myblockbuster.core.User
import com.myblockbuster.core.services.Service
import com.myblockbuster.movies.Movie
import com.myblockbuster.movies.MovieAlreadyExistsException
import com.myblockbuster.movies.MovieNotExistsException
import kotlin.math.min

class MovieService: Service<Movie, User> {

    // This is because we don't have a persistence layer yet
    // Part 6 will explain that
    val elements: MutableList<Movie> = ArrayList()

    override fun create(user: User, element: Movie): Movie {
        if (elements.find { it.code == element.code } == null)
            elements.add(element)
        else
            throw MovieAlreadyExistsException()

        return element
    }

    override fun findAll(user: User, filters: Map<String, Any>, pagination: Pagination): Page<Movie> {
        val offset = min(pagination.page * pagination.size, elements.size - 1)
        val toIndex = min(offset + pagination.size, elements.size - 1)
        return Page(filters, pagination.size, offset, elements.size,
                if (elements.isEmpty()) elements else elements.subList(offset, toIndex))
    }

    override fun findOne(user: User, id: Int): Movie {
        val movie: Movie? = elements.find { it.id == id}
        if (movie != null)
            return movie
        else
            throw MovieNotExistsException()
    }

    override fun findOne(user: User, filters: Map<String, Any>): Movie {
        TODO("not implemented")
    }

    override fun update(user: User, element: Movie): Movie {
        val movie = findOne(user, element.id!!)
        movie.cast = element.cast
        movie.code = element.code
        movie.director = element.director
        movie.language = element.language
        movie.rate = element.rate
        movie.title = element.title

        return movie
    }

    override fun delete(user: User, id: Int) {
        val movie = findOne(user, id)
        elements.remove(movie)
    }

    override fun count(user: User, filters: Map<String, Any>): Int {
        return elements.size
    }

    override fun exists(user: User, id: Int): Boolean {
        return elements.find { it.id == id} != null
    }

}

Movie Controller Implementation

package com.myblockbuster.movies.controllers

import com.myblockbuster.core.Request
import com.myblockbuster.core.User
import com.myblockbuster.core.controllers.Controller
import com.myblockbuster.core.services.Service
import com.myblockbuster.movies.Movie
import com.myblockbuster.movies.services.MovieService

class MovieController: Controller<Movie> {

    fun movie(request: Request?) : Any {
        val service: Service<Movie, User> = MovieService()
        return defaultRouting(Movie::class.java, request!!, service)
    }
}

Now just gradle deploy and test it! You should be able to “CRUD” a movie!

BONUS: Using the Factory pattern to behave like Spring

If you don’t like highly coupled relationships, using a factory design pattern to interact with your services is a good choice. As I already mentioned, the use of reflection, Aspect Oriented Programming (AOP) or any code injection can affect the performance of your Lambda function. So using these basic and well-known patterns is always the right way to go.

Service Factory

We can define an interface and its implementation for the Service Factory. In this specific case, the service is being returned based on the model we want to CRUD.

package com.myblockbuster.core.factories

import com.myblockbuster.core.User
import com.myblockbuster.core.services.Service
import com.myblockbuster.movies.Movie
import com.myblockbuster.movies.services.MovieService

/**
 * Interface Factory
 */
interface Factory

/**
 * Creates a Service according to the Entity type to manage
 */
class ServiceFactory<T>: Factory {

    fun <T : Any> getService(type: Class<T>): Service<T, User> {
        return when (type) {
            Movie::class.java -> MovieService() as Service<T, User>
            else             -> throw IllegalArgumentException()
        }
    }
}

Movie Controller Implementation with Service Factory

package com.myblockbuster.movies.controllers

import com.myblockbuster.core.Request
import com.myblockbuster.core.User
import com.myblockbuster.core.controllers.Controller
import com.myblockbuster.core.factories.ServiceFactory
import com.myblockbuster.core.services.Service
import com.myblockbuster.movies.Movie

class MovieController: Controller<Movie> {

    fun movie(request: Request?) : Any {
        val service: Service<Movie, User> = ServiceFactory<Movie>().getService(Movie::class.java)
        return defaultRouting(Movie::class.java, request!!, service)
    }
}

SOURCE CODE

If you want to see the complete example, the source code is available at this Github repo.

Final Thoughts

This article presented an implementation of a Client-Dispatcher-Server pattern to use in your Kotlin + Lambda Functions as Microservices and an HTTP Micro-router for your CRUD requests. This is a quick example of a library/framework that I’m developing to rock future projects. A framework that pretends to remove all the boilerplate code for typical Kotlin Serverless APIs projects.

The next articles will explain in detail how to create some good unit tests for Kotlin in AWS Lambda, add a relational persistence layer and finally how to integrate security requirements that we may need.

Originally posted at Medium

Subscribe to our newsletter to get the latest product updates, tips, and best practices!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.