Over a million developers have joined DZone.

Simplicity and Adaptability in API Design

DZone's Guide to

Simplicity and Adaptability in API Design

Get some tips and best practices for making more efficient and understandable design choices in your APIs, to make them more usable.

· Integration Zone ·
Free Resource

Discover how you can get APIs and microservices to work at true enterprise scale.

How do you like your model and API? Detailed with separate classes, or more uniform and lightweight with fewer classes, but with heavier objects, where some of the properties are optional? Well, there is a delicate line in interface design between simplicity and adaptability, and it is up to you to decide on what is a viable design- just enough design to satisfy your needs, but keeping in mind you might need to extend it in the future. Let's step up for an example and see what I'm talking about. This is an open discussion; I like to hear thoughts and refine and tune my thinking, so I hope you have much to say about it. We are using the Scala language for compactness of the example, but this does not mean it's useful only for Scala- it's just an example.

Step 1: Simple Single Data Class

We'll start by exploring a simple design. We have a process and we want to report on it's progress, therefore, our domain model will communicate that we have a process together with its relevant properties. We are following the Domain Driven Design concept- the bounded context.

In this first step, we are going to create a single case class which will hold our properties for the process update.


Step 2: Extract Interface

This is very limited. What if we want to extend that process update, if we have more of that kind of process update? Let's start extending it by adding an interface, which is a good practice anyway.

trait ProcessUpdate {
  def name: String
  def id: String
  def message: String

case class ProcessUpdate(id: String, name: String, message: String, status: String)

Step 3: Add Support for Errors

Now, what if our client wants to update us about an error. How would he do that? We have a few choices to make here. Let's go with the naive solution and add support for reporting the exception (we are not using Failure because we are utilizing Scala for the sake of shortness, not it's specific classes).

So, we natively add the error to the interface and to the case class:

sealed trait ProcessUpdate {
  def name: String
  def id: String
  def message: String
  def error: Option[Exception] // We are exploring where we should add the error should we add it to one single interface and case class?

case class ProcessUpdate(id: String, name: String, message: String, status: String, error: Option[Exception]) extends ProcessUpdate

def reportProcessStatus(processUpdate: ProcessUpdate)

Now the user can provide the Exception if he had one or None in the case he does not have.

Is that good?

Well, imagine we have more such cases, of optional fields. We are going to pile them up on our poor interface. Moreover, the client who uses our case class would need to think over many params for what he should pass, and whether it's OK or not that he passes None. This leads to a poor interface and mixed up users of our API.

Precaution: Note that although we try to stay away from this way of extending our domain, I will never tell you that this is 100% incorrect. We should always consider each case on its own.

Step 4: Domain Model on ADT

In this case, say we have a general ProcessUpdate interface and we could utilize it in multiple ways. One is success, and one is error, and only in the error case would we need to pass the exception. That comes with an additional case class we need to provide, and additional mental stress on the client, who needs to remember to use one case class in one case and another in another case. But, we are reducing his mental stress by specifying each such modeled class in our API, so he would know exactly what to pass.

sealed trait ProcessUpdate {
  def name: String
  def id: String
  def message: String

case class SuccessProcessUpdate(id: ExecutionIname: String, id: String, message: String) extends ProcessUpdate
case class FailureProcessUpdate(name: String, id: String, message: String, e: Exception) extends ProcessUpdate

def reportProcessSuccess(successUpdate: SuccessProcessUpdate) // success? pass success data.
def reportProcessFailed(failureProcessUpdate: FailureProcessUpdate) // it's now clear in fail we pass fail.

That's better. Now, if we have a method to allow the user to report an error, we would ask him to provide a FailureProcessUpdate, and in this case, we have no optional exception.

The Question

The question is, of course, where do you put an end to this? You might find yourself with an explosion of case classes for each case (here we just handled the error case).  The answer to that would be that you need to consider your options; meaning, if you have an explosion, think about your design. Maybe those are different domain objects and traits altogether and you should be combining them.

The Key

Now, I kept this until the end, but this should be written at the beginning- I just didn't want to tell you the "secret" right from the start. The key to successful design is to try the design yourself. So it's almost a magic bullet, if you think, "Which design should I use?" like in our example, just write unit tests as first clients to both designs, and while you write your unit tests, you will see that one simply makes more sense! Choose that design!

In addition, we are just touching upon this concept, but I thought it was important to raise any design decisions up, as trivial as they may be. Sometimes we just take things for granted. I strongly promote that we should always consider the different options and choose the right one consciously. If you want to go deeper into this topic and other design decisions, there is an absolutely amazing book named Scala Design Patterns. This is one of the best books I have found on Ccala, taking many concepts and specifically discussing how we should approach them.

APIs and microservices are maturing, quickly. Learn what it takes to manage modern APIs and microservices at enterprise scale.

scala ,api ,api design ,integration

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}