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
}
)
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
})
}
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))
}
For your tests you can simply implement a testObserver:
type testObserver struct {
event Event
}
func (e *testObserver) NotifyCallback(event Event) {
e.event = event
}
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"})
}
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")
})
}
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)
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.
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.