ZIO DynamoDB

ZIO DynamoDB

  • Overview
  • Use Cases
  • About

›Overview

Overview

  • Overview
  • Why ZIO-DynamoDB
  • Batching and Parallelisation
  • Parallelisation

Why ZIO-DynamoDB

ZIO-DynamoDB provides one of the best boilerplate free APIs for interacting with DynamoDB. Compared to the Java SDK you will see a massive reduction in code that you need to maintain. Below we have an example spread out across a few different code blocks. Each block is an incremental step towards using DynamoDB with the Java SDK.

final case class Student(
  email: String,            // partition key 
  subject: String,          // sort key
  enrollmentDate: Option[Instant], 
  payment: Payment
)

sealed trait Payment
object Payment {
  final case object DebitCard  extends Payment
  final case object CreditCard extends Payment
  final case object PayPal     extends Payment
}

Our case class for the example. We plan on writing a few students to DynamoDB. Note we have an optional field, enrollmentDate as well as a sealed trait payment.

In order to write a Student to DynamoDB we need to build a PutItemRequest for our case class.

def putStudentRequest(student: Student): PutItemRequest =
  PutItemRequest.builder
    .tableName("student")
    .item(toAttributeValueMap(student).asJava) //Item: Map[String, AttributeValue]
    .build

def toAttributeValueMap(student: Student): Map[String, AttributeValue] = {
  val mandatoryFields                                     = Map(
    "email"   -> AttributeValue.builder.s(student.email).build,
    "subject" -> AttributeValue.builder.s(student.subject).build,
    "payment" -> AttributeValue.builder.s { // serialize sum type
      student.payment match {
        case DebitCard  => "DebitCard"
        case CreditCard => "CreditCard"
        case PayPal     => "PayPal"
      }
    }.build
  )

  val nonEmptyOptionalFields: Map[String, AttributeValue] = Map(
    "enrollmentDate" -> student.enrollmentDate.map(instant =>
      AttributeValue.builder.s(instant.toString).build)
  ).filter(_._2.nonEmpty).map { case (k, v) => (k, v.get) }

  mandatoryFields ++ nonEmptyOptionalFields
}

Here we have written some helper functions to serialize a Student into a PutItemRequest for the Java SDK.

Now that we can write a Student to DynamoDB we need to be able to read them back and deserialize them.

def getStudentRequest(email: String, subject: String): GetItemRequest = {
  GetItemRequest.builder
    .tableName("student")
    .key(
      Map(
        "email" -> AttributeValue.builder.s(email).build,
        "subject" -> AttributeValue.builder.s(subject).build
      ).asJava
    )
    .build()
}

def getString(map: Map[String, AttributeValue],
              name: String): Either[String, String] =
  map.get(name).toRight(s"mandatory field $name not found").map(_.s)

def getStringOpt(map: Map[String, AttributeValue],
                 name: String): Either[Nothing, Option[String]] =
  Right(map.get(name).map(_.s))

def parseInstant(s: String): Either[String, Instant] =
  Try(Instant.parse(s)).toEither.left.map(_.getMessage)

def attributeValueMapToStudent(item: Map[String, AttributeValue])
: Either[String, Student] =
  for {
    email                 <- getString(item, "email")
    subject               <- getString(item, "subject")
    maybeEnrollmentDateAV <- getStringOpt(item, "enrollmentDate")
    maybeEnrollmentDate   <-
      maybeEnrollmentDateAV.fold[Either[String, Option[Instant]]](Right(None))(s =>
        parseInstant(s).map(i => Some(i))
      )
    payment               <- getString(item, "payment")
    paymentType           <- payment match {
      case "DebitCard"  => Right(DebitCard)
      case "CreditCard" => Right(CreditCard)
      case "PayPal"     => Right(PayPal)
      case _            => Left("invalid payment type")
    }
  } yield Student(email, subject, maybeEnrollmentDate, paymentType)

In this code block we have setup a number of functions to assist with creating a GetItemRequest as well as deserialize the response to a Student.

Let's put all of these functions together to create a full program.

val program = 
for {
  client            <- ZIO.service[DynamoDbAsyncClient]
  student            = Student("avi@gmail.com", "maths", Instant.now, 
                       Payment.DebitCard)

  putRequest         = putStudentRequest(student)
  _                 <- ZIO.fromCompletionStage(client.putItem(putRequest))
  getItemRequest     = getStudentRequest(student.email, student.subject)
  getItemResponse   <- ZIO.fromCompletionStage(client.getItem(getItemRequest))
  studentItem        = getItemResponse.item.asScala.toMap
  foundStudent       = deserialise(studentItem)
} yield foundStudent

Excluding the case class definition and the full program, that's 65 additional lines of code exclusively for handling writing to and reading from DynamoDB. It doesn't even begin to cover the more complex query types, automatic batching, and automatic parallaisation that ZIO-DynamoDB provides.

Let's look at the same program using ZIO-DynamoDB instead of using the Java SDK.

val liveDynamoDBExecutorLayer = ...

object Student {
  implicit lazy val schema: Schema[Student] = DeriveSchema.gen[Student]
}

val errorOrListOfStudent = (for {
  avi          = Student("avi@gmail.com", "maths", ...)
  adam         = Student("adam@gmail.com", "english", ...)

  _            <- (put[Student]("student", avi) zip put[Student]("student", adam)).execute

  listOfErrorOrStudent <- forEach(List(avi, adam)) { st =>
      get[Student]("student",
        PrimaryKey("email" -> st.email, "subject" -> st.subject)
      )}.execute

} yield EitherUtil.collectAll(listOfErrorOrStudent))
  .provideLayer(liveDynamoDBExecutorLayer)

ZIO-DynamoDB leverages ZIO-Schema to provide serialization for developers out of the box. The amount of boilerplate code ZIO-DynamoDB removes for your code base means engineers have more time writing business logic and less time maintaining their integration with DynamoDB.

There was also a talk at Functional Scala 2021 that covers the drastic reduction in code that results from migrating to ZIO-DynamoDB from the Java SDK.

← OverviewBatching and Parallelisation →
ZIO DynamoDB
GitHub
Star
Chat with us on Discord
discord
Additional resources
Scaladoc of zio-dynamodb
Copyright © 2022 ZIO Maintainers