DEV Community

Cover image for Using design patterns in AWS Lambda
Joris Conijn for AWS Community Builders

Posted on • Originally published at xebia.com on

Using design patterns in AWS Lambda

When you speak with software developers, they will probably tell you that they use design patterns. But when the world first shifted to the internet the general feeling was that these design patterns would not work for the web. This is not true, and today you see these patterns being used more and more.

I have noticed the same behavior with serverless. In this blog post I will go over some reasons why you should be using design patterns in your Lambda functions

Getting started

To get started with AWS Lambda is quite easy, and this is also the reason why some crucial steps are skipped. For example, you can use the console to create a function and type your code in an editor via your browser. Or, you create a CloudFormation template and you put your code in the template itself. Obviously, this is fine for experimentation and learning purposes. But when you start developing systems that need to be reliable you need to take a different approach.

The biggest objection that I have with the editor in the console is that it does not allow you to run tests. Also using inline code that is part of a CloudFormation template has some downsides. Again you cannot run tests against your inline code and the code becomes vulnerable to indenting and syntax errors due to the lack of proper IDE highlighting.

Both options do not allow you to add dependencies, or at least not in an easy way. So you default to the installed dependencies, and you have no control over these dependencies.

Separate your infrastructure and application code

By storing your application code in separate files you will have all the benefits of using an IDE. You can run linters, formatters and tests and you have syntax highlighting while you develop your code. This also allows you to run these steps in the CI/CD pipeline before you actually deploy your code to production. This gives you a quality gate and a decision moment to say yes this can go to production.

When you combine this with the AWS Serverless Application Model you can also very easily include your dependencies. Or use a compiled language like golang for your Lambda functions. You simply run sam build before you run the aws cloudformation package and aws cloudformation deploy commands. SAM will build the binary and update the template to point to the newly built binary. Package will then upload it to S3 and replace the local reference to the S3 location. Deploy can then create or update the stack or you can use the CloudFormation integration in CodePipeline.

Why should I use design patterns

Testing AWS calls can be challenging, I wrote a blog on this for golang. By splitting the business logic from the infrastructure in your code you gain 2 important things:

  • You can focus on the business logic without having to deal with stubbing your infrastructure.
  • The used infrastructure can be changed without the need to change your business logic.

The former makes it easier to add test scenarios. When it’s easy to add scenarios you are more likely to add more scenarios. Increasing the reliability of your business logic. The latter gives you flexibility, you can swap a RDS database with a DynamoDB table relatively easily. Without changing the business logic, this also means that you have less of a vendor lock-in.

Golang example

Let’s use the Observer pattern, first you will need some interfaces:

type (
   Event struct {
      name string
   }

   Observer interface {
      NotifyCallback(Event)
   }

   Subject interface {
      AddListener(Observer)
      RemoveListener(Observer)
      Notify(Event)
   }

   eventObserver struct {
      id int
      time time.Time
   }

   eventSubject struct {
      observers sync.Map
   }
)

Enter fullscreen mode Exit fullscreen mode

In this example we define an Event, Subject and an Observer. We can use the event to store all the relevant data. When you pass the Event in the Subject the Event is sent to all the registered observers.

Next we need some logic to register the observers on the Subject:

func (s *eventSubject) AddListener(observer Observer) {
   s.observers.Store(observer, struct{}{})
}

func (s *eventSubject) RemoveListener(observer Observer) {
   s.observers.Delete(observer)
}

func (s *eventSubject) Notify(event Event) {
   s.observers.Range(func(key interface{}, value interface{}) bool {
      if key == nil || value == nil {
         return false
      }

      key.(Observer).NotifyCallback(event)
      return true
   })
}

Enter fullscreen mode Exit fullscreen mode

As you can see the Notify method will iterate over all observers and pass the event to all registered observers. The observer could be as simple as:

func (e *eventObserver) NotifyCallback(event Event) {
   fmt.Printf("Received from observer %d: %s after %v\n", e.id, event.name, time.Since(e.time))
}

Enter fullscreen mode Exit fullscreen mode

For your tests you can simply implement a testObserver:

type testObserver struct {
   event Event
}

func (e *testObserver) NotifyCallback(event Event) {
   e.event = event
}

Enter fullscreen mode Exit fullscreen mode

This is a very simplified version of the business logic:

func doStuff(observer Observer) {
   n := eventSubject{observers: sync.Map{}}
   n.AddListener(observer)
   n.Notify(Event{name: "Joris Conijn"})
}

Enter fullscreen mode Exit fullscreen mode

We create an Event with Joris Conijn as the name value. We want to test this business logic that it indeed creates this Event with Joris Conijn as the name value.

func TestObserver(t *testing.T) {
   t.Run("Run doStuff", func(t *testing.T) {
      var obs1 = testObserver{}
      doStuff(&obs1)
      assert.Equal(t, obs1.event.name, "Joris Conijn")
   })
}

Enter fullscreen mode Exit fullscreen mode

For the actual implementation the observer would store it in DynamoDB or RDS. To have a good testing coverage you do need to test the observer. Per observer you now test the actual implementation of the database of your choice.

Conclusion

You can use design patterns in your Lambda functions. It is a great way to separate your business logic from the used infrastructure. And it makes you business logic easier to test. However, you might have noticed that you will need some boilerplate code to implement these patterns.

My advice would be to use these patterns when you need portability and/or when you have more complex business logic. For very simple Lambda functions it might not be worth it.

Thanks Tensor Programming for the inspiration. Photo by Soloman Soh

The post Using design patterns in AWS Lambda appeared first on Xebia.

Top comments (2)

Collapse
 
rdarrylr profile image
Darryl Ruggles

Excellent points and I agree that many people developing today have thrown out a lot of the good approaches we have learned over the last 20-30 years just because we have almost infinite scale in the cloud.

Collapse
 
nr18 profile image
Joris Conijn

Indeed, the same applies to memory and cpu usage. Just because the machines got better the need of proper designing your software was ignored more and more.

Hopefully sustainability will bring this back, one can only hope.