Blog

Verwendung von Entwurfsmustern in AWS Lambda

Joris Conijn

Aktualisiert Oktober 15, 2025
5 Minuten

Wenn Sie mit Softwareentwicklern sprechen, werden diese Ihnen wahrscheinlich sagen, dass sie Design Patterns verwenden. Aber als die Welt sich zum ersten Mal auf das Internet umstellte, herrschte die allgemeine Meinung, dass diese Entwurfsmuster für das Web nicht funktionieren würden. Das stimmt nicht, und heute sehen Sie, dass diese Muster mehr und mehr verwendet werden.

Ich habe dasselbe Verhalten bei Serverless festgestellt. In diesem Blogbeitrag gehe ich auf einige Gründe ein, warum Sie in Ihren Lambda-Funktionen Entwurfsmuster verwenden sollten

Erste Schritte

Der Einstieg in AWS Lambda ist recht einfach, und das ist auch der Grund, warum einige entscheidende Schritte übersprungen werden. Sie können zum Beispiel die Konsole verwenden, um eine Funktion zu erstellen und Ihren Code über Ihren Browser in einen Editor eingeben. Oder Sie erstellen eine CloudFormation-Vorlage und fügen Ihren Code in die Vorlage selbst ein. Für Experimente und Lernzwecke ist das natürlich gut geeignet. Aber wenn Sie anfangen, Systeme zu entwickeln, die zuverlässig sein müssen, müssen Sie einen anderen Ansatz wählen.

Der größte Einwand, den ich gegen den Editor in der Konsole habe, ist, dass Sie damit keine Tests durchführen können. Auch die Verwendung von Inline-Code, der Teil einer CloudFormation-Vorlage ist, hat einige Nachteile. Auch hier können Sie keine Tests mit Ihrem Inline-Code durchführen und der Code ist anfällig für Einrückungs- und Syntaxfehler, da die IDE ihn nicht richtig hervorhebt.

Bei beiden Optionen können Sie keine Abhängigkeiten hinzufügen, zumindest nicht auf einfache Art und Weise. Sie verwenden also standardmäßig die installierten Abhängigkeiten, und Sie haben keine Kontrolle über diese Abhängigkeiten.

Trennen Sie Ihren Infrastruktur- und Anwendungscode

Wenn Sie Ihren Anwendungscode in separaten Dateien speichern, haben Sie alle Vorteile einer IDE. Sie können Linters, Formatierer und Tests ausführen und haben eine Syntaxhervorhebung, während Sie Ihren Code entwickeln. Außerdem können Sie diese Schritte in der CI/CD-Pipeline ausführen, bevor Sie Ihren Code tatsächlich in die Produktion überführen. Damit haben Sie eine Qualitätskontrolle und einen Entscheidungszeitpunkt, an dem Sie sagen können: Ja, das kann in Produktion gehen.

Wenn Sie dies mit dem AWS Serverless Application Model kombinieren, können Sie auch ganz einfach Ihre Abhängigkeiten einbinden. Oder Sie verwenden eine kompilierte Sprache wie golang für Ihre Lambda-Funktionen. Sie führen einfach sam build aus, bevor Sie die Befehle aws cloudformation package und aws cloudformation deploy ausführen. SAM erstellt die Binärdatei und aktualisiert die Vorlage so, dass sie auf die neu erstellte Binärdatei verweist. Das Paket lädt es dann auf S3 hoch und ersetzt den lokalen Verweis auf den S3-Speicherort. Deploy kann dann den Stack erstellen oder aktualisieren oder Sie können die CloudFormation-Integration in CodePipeline verwenden.

Warum sollte ich Entwurfsmuster verwenden?

Das Testen von AWS-Aufrufen kann eine Herausforderung sein. Ich habe dazu einen Blog für golang geschrieben. Durch die Trennung der Geschäftslogik von der Infrastruktur in Ihrem Code gewinnen Sie 2 wichtige Dinge:

  • Sie können sich auf die Geschäftslogik konzentrieren, ohne sich um das Stubbing Ihrer Infrastruktur kümmern zu müssen.
  • Die verwendete Infrastruktur kann geändert werden, ohne dass Sie Ihre Geschäftslogik ändern müssen.

Ersteres macht es einfacher, Testszenarien hinzuzufügen. Wenn es einfach ist, Szenarien hinzuzufügen, ist die Wahrscheinlichkeit größer, dass Sie weitere Szenarien hinzufügen. Das erhöht die Zuverlässigkeit Ihrer Geschäftslogik. Letzteres gibt Ihnen Flexibilität, denn Sie können eine RDS-Datenbank relativ einfach gegen eine DynamoDB-Tabelle austauschen. Ohne die Geschäftslogik zu ändern, bedeutet dies auch, dass Sie weniger an einen bestimmten Anbieter gebunden sind.

Golang Beispiel

Lassen Sie uns das Observer-Muster verwenden. Zunächst benötigen Sie einige Schnittstellen:

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 diesem Beispiel definieren wir ein Ereignis, ein Subjekt und einen Beobachter. Wir können das Ereignis verwenden, um alle relevanten Daten zu speichern. Wenn Sie das Ereignis im Betreff übergeben, wird das Ereignis an alle registrierten Beobachter gesendet.

Als Nächstes brauchen wir eine Logik, um die Beobachter für das Subjekt zu registrieren:

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
   })
}

Wie Sie sehen können, durchläuft die Notify-Methode alle Beobachter und leitet das Ereignis an alle registrierten Beobachter weiter. Der Beobachter könnte so einfach sein wie:

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

Für Ihre Tests können Sie einfach einen testObserver implementieren:

type testObserver struct {
   event Event
}

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

Dies ist eine sehr vereinfachte Version der Geschäftslogik:

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

Wir erstellen ein Ereignis mit Joris Conijn als Namenswert. Wir möchten testen, ob die Geschäftslogik tatsächlich dieses Ereignis mit Joris Conijn als Namenswert erzeugt.

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")
   })
}

Bei der eigentlichen Implementierung würde der Beobachter die Daten in DynamoDB oder RDS speichern. Um eine gute Testabdeckung zu erreichen, müssen Sie den Beobachter testen. Pro Beobachter testen Sie nun die eigentliche Implementierung der Datenbank Ihrer Wahl.

Fazit

Sie können Entwurfsmuster in Ihren Lambda-Funktionen verwenden. Dies ist eine großartige Möglichkeit, Ihre Geschäftslogik von der verwendeten Infrastruktur zu trennen. Gleichzeitig erhalten Sie einfachere Möglichkeiten zum Testen Ihrer Geschäftslogik. Vielleicht haben Sie aber auch schon bemerkt, dass Sie für die Implementierung dieser Muster einen gewissen Boilerplate-Code benötigen. Ich würde Ihnen raten, diese Muster zu verwenden, wenn Sie Portabilität benötigen und/oder wenn Sie eine komplexere Geschäftslogik haben.

Vielen Dank an Tensor Programming für die Inspiration. Foto von Soloman Soh

Verfasst von

Joris Conijn

Joris is the AWS Practise CTO of the Xebia Cloud service line and has been working with the AWS cloud since 2009 and focussing on building event-driven architectures. While working with the cloud from (almost) the start, he has seen most of the services being launched. Joris strongly believes in automation and infrastructure as code and is open to learning new things and experimenting with them because that is the way to learn and grow.

Contact

Let’s discuss how we can support your journey.