Exploring Variations of Implementing Domain Driven Design With The “Ports and Adapters” Pattern, Part 1
Hexagonal Architecture is a key design pattern to use when implementing Domain Driven Design. It enables evolutionary changes, helps to keep test suites fast and reliable, and protects the system against ripple effects caused by technology issues. This series of blog posts explores its origin and benefits, as well as five possible implementations and their individual traits.
0. Before we begin: Why Domain Driven Design?
The idea of separating business logic from the rest of a system isn’t exactly new: Separation of concerns as a design principle has been around at least since the 1970s1 , as a means of creating modular, composable computer program. And since the business arguably represents a unique concern, which can be clearly distinguished from others, such as security or networking, it is only logical that we should be able to find ways of encapsulating, abstracting, and protecting it in our implementation code.
In Eric Evans’ seminal book “Domain Driven Design”2 (a.k.a. “The Blue Book”), he adds an important philosophical aspect:
“The part of the software that specifically solves problems from the domain usually constitutes only a small portion of the entire software system, although its importance is disproportionate to its size3 . To apply our best thinking, we need to be able to look at the elements of our model and see them as a system. We must not be forced to pick them out of a much larger mix of objects, like trying to identify constellations in the night sky. We need to decouple the domain objects from other functions of the system, so we can avoid confusing the domain concepts with other concepts related only to software technology or losing sight of the domain altogether in the mass of the system”
This understanding of domain-related code as the crucial element of solving business problems is the fundamental principle of Domain Driven Design: We strive to think, model and implement essential business logic of a system in a cleanly separated, isolated space – the Domain Model. All other aspects of a software system are supporting extensions to this model, and should be treated as such.
However, any reasonably large software system often contains complex enough business logic to make implementing a single coherent model hard, if not impossible: Different organisational units work with different understandings both of their specialised area of expertise (a.k.a. subdomain), and of the rest of the system. Their mental models of the business are often partial, contain contradictions, misunderstandings and conflicting terminology. People in an organisation simply have a way of making things work despite all of these shortcomings (and sometimes even because of them). And most of the time, they don’t even notice all the small corrections and adjustments they make in the process.
For an executable model, this just won’t do. As Eric Evans notes,
“The goal of the most ambitious enterprise system is a tightly integrated system spanning the entire business. Yet the entire business model for almost any such organisation is too large and complex to manage or even understand as a single unit. The system must be broken into smaller parts, in both concept and implementation.”
And thus we further separate the domain into multiple Bounded Contexts , each represented by its own dedicated Domain Model.
1. The classic approach: Layered architecture
To keep the domain separate from technological concerns, Evans recommends implementing a layered architecture , containing these four layers:
- Presentation layer (UI, view, graphical interactions)
- Application layer (a.k.a. service layer, containing APIs and “jobs”, i.e. use cases4 )
- Domain layer (a.k.a. business layer)
- Infrastructure layer (persistence, message passing, architectural frameworks)
The layers are stacked onto each other according to how they depend on each other: Each layer must only ever depend on itself, and the layers below. This is intended to allow for modularity and changeability.
Consequently, a reference implementation should look something like this:
Note: Certainly, a presentation layer could be divided into several, possibly overlapping parts (e.g., a web UI and a mobile app, showing different parts of the system to different users). And the infrastructure layer is not homogeneous either, but rather composed of a number of different purpose modules. However, since their structure is governed by other aspects, they are not part of the Bounded Context – and I’ve chosen to show them as a single layer to make the diagram easier to read.
2. What’s wrong with layers?
The Blue Book was written in 2004, and at the time, just about any web based application was written using layered architecture. It was “state of the art” back then, recommended and discussed extensively in Martin Fowler’s highly influential book “Patterns of Enterprise Application Architecture”5 . It was also not the only way to write applications back then, and it certainly isn’t today.
Eric Evans even says in the book, that sometimes it’s advisable to build what he calls “Smart UI” applications:
”Therefore, when circumstances warrant:
Put all the business logic into the user interface. Chop the application into small functions and implement them as separate user interfaces, embedding the business rules into them. Use a relational database as a shared repository of the data. Use the most automated UI building and visual programming tools available.”
But makes sure to clarify:
”Smart UI is an alternate, mutually exclusive fork in the road, incompatible with the approach of domain-driven design.”
The crucial factors, which should influence the decision whether to apply Domain Driven Design, are 1) the perceived complexity of the problem space (i.e., the business logic), and 2) the technical and analytical maturity of the implementation team: Successful DDD benefits greatly from many advanced software development techniques (such as BDD , TDD , refactoring , and the intentional use of connascence ). Moreover, naive attempts at strategic design , without an experienced guide or facilitator, can easily make things worse, instead of better.
But even after the decision is made to go with Domain Driven Design, some aspects of layered architecture still remain a potential problem:
- Dependencies may point downward only, but even downward dependencies remain a liability. For example, in the layered style, a change in the database structure will almost certainly cause redeployment of the entire stack: The business layer depends on it, the application layer depends on the business layer and the database, the GUI depends on the application layer – we call this a “ripple effect .”
- Because the database is at the bottom of the stack, and changing it has major consequences (not the least of which are significant costs), we tend to design it first, and protect its design from changing too often. Thus, data modelling — especially for relational data — inadvertently shapes and informs (sometimes unconsciously) our thinking about the structure of business objects. This can ultimately cause the design of said business objects to mimic the design of a normalised relational database, in the worst case. It may lead to inferior, sometimes even harmful architecture decisions, which negatively impact the entire system. Some examples are the partitioning of distributed systems by data ownership, which leads to the “Distributed Monolith ” anti-pattern, and the dreaded “Canonical Data Model .”
- Once the decision for a particular database technology is made, it is very hard to replace. While object-relational mappers, such as Hibernate, may allow us to swap an in-memory database (h2) for the production db (MariaDB cluster) during testing, a running database is still necessary — and tests become significantly slower. Switching the technology model used for persistence in production, then, becomes a major undertaking that can take weeks, months, or even years to achieve. And this may become necessary sooner than one would think, due to changed performance requirements, network restrictions, or regulatory changes — be it from SQL to NoSQL, or to in-memory- or file-based storage.
3. Enter Hexagonal Architecture
Shortly after the publication of the Blue Book, in 2005, Alistair Cockburn published the original version of what he called the Hexagonal Architecture , a.k.a. the Ports and Adapters pattern. Alistair had previously published a book on Writing Effective Use Cases6 , and had his own issues with finding business code scattered across the system – and with the effects of layered architecture:
“One of the great bugaboos of software applications over the years has been infiltration of business logic into the user interface code. The problem this causes is threefold:
- First, the system can’t neatly be tested with automated test suites because part of the logic needing to be tested is dependent on oft-changing visual details such as field size and button placement;
- For the exact same reason, it becomes impossible to shift from a human-driven use of the system to a batch-run system;
- For still the same reason, it becomes difficult or impossible to allow the program to be driven by another program when that becomes attractive.
The attempted solution, repeated in many organisations, is to create a new layer in the architecture, with the promise that this time, really and truly, no business logic will be put into the new layer. However, having no mechanism to detect when a violation of that promise occurs, the organisation finds a few years later that the new layer is cluttered with business logic and the old problem has reappeared.”
He also found a symmetry between the GUI and another part of the application, which corresponds to the infrastructure layer:
”An interesting similar problem exists on what is normally considered “the other side” of the application, where the application logic gets tied to an external database or other service. When the database server goes down or undergoes significant rework or replacement, the programmers can’t work because their work is tied to the presence of the database. This causes delay costs and often bad feelings between the people.
It is not obvious that the two problems are related, but there is a symmetry between them that shows up in the nature of the solution.”
Alistair’s solution is as simple as it is radical: He proposes to forego the concept of layers entirely, and instead expose the core of the application — the business — to all external concerns via “ports”, i.e. a public API fit to each individual purpose. Any number of “adapters ” can connect to these, and work seamlessly as a whole, leaving the core itself oblivious to technical details and the exact nature of the external parts. This logic can be applied both to what he calls the “left side” of the application — the “driving”, or event handling ports used by the GUI, batch mechanisms or remotely connected controlling applications, such as ERP systems, and also to the “right side”, i.e. the “driven” ports, used for connecting persistence, messaging and remotely controlled applications.
A reference implementation could be drawn like this7 :
Note: The external adapters depend on protocols/interfaces provided by the core. On the left side, the interface is used by the driving adapters, on the right side, the interface is implemented by the driven adapters. All dependencies point inward, such that the core itself can remain entirely oblivious of its surroundings.
Even though Alistair probably didn’t have Domain Driven Design in mind, when he came up with the pattern (it isn’t mentioned in the article), there can be little doubt it is a perfect fit: It preserves the original intention of separation of concerns — moreover, it enforces an even stricter separation of the business than the layered approach — and it solves all the issues with layered architecture listed in the previous section. Moreover, its technology-agnostic nature also allows it to be implemented in many ways, providing a variety of options, according to the scale and monolithic/distributed nature of the intended product.
In the remainder of this series of blog posts, we will explore and compare some possible implementations and their individual advantages and drawbacks.
4. The scenario: A web shop with ambitions
How does one demonstrate the effects of a large scale implementation pattern, which shows its true value over time?
My answer is: By showing several stages of an evolutionary change, from simple to complicated, from monolithic to distributed, from relational data storage to event sourcing. Changes that would naturally occur in an uprising business, when you transition from “let’s just get it to work” to “we can’t keep up with the demand, and we urgently need more insights into our users’ wants and needs, to keep our product portfolio attractive.”
The scenario is one we should all be fairly familiar with: Our small startup has an online shop with a catalog of products, from which a user can add items to a shopping cart (or remove them), which is eventually taken to checkout and becomes an order. Luckily, the shop has been gaining a lot of traction, and we want to make it scale massively, soon. For now, we have two frontend applications: A shop GUI, for customers, and an admin GUI for managing the products and keeping track of open carts and orders.
We have thought about licensing an ERP system, which might replace our product management application with a standard logistics platform, and likewise, we might connect to an external service to give us GTIN product codes. But at this time, these are just ideas to keep in mind.
We have done an EventStorming workshop, and this is the output we created:
The domain really has only one bounded context deserving the title, “Shopping Cart”, along with two adjacent contexts, which merely use CRUD logic: “Products”, basically the source of master data, and “Orders”, the bucket in which we store our end product. There is only one transition across the context boundary, wherein a shopping cart is checked out and transformed into an order. There are three business rules (policies), all concerning the lifecycle of shopping carts and orders, and we’ve identified an issue where we want to make sure items are only added to a shopping cart, if they refer to a product that is actually in the catalog (in the past, we’ve had problems where people clicked on cached items in their browser, resulting in orders we couldn’t fulfill due to availability issues).
This domain, and the GUIs, will remain more or less the same throughout all five example implementations – but the structure and deployment of the backend will change significantly, in five steps, from a single-server application to a fully distributed, eventually consistent system:
- A modular monolith: This is our starting point and initial tactical design.
- Shared kernel “microservices”: We keep the domain core as a single library, but change the deployment model.
- Microservices, “for Real”: We split the domain core into share-nothing components.
- CQRS: We divide our APIs into write- and read-related components to allow for better scaling.
- EventSourcing: We replace our relational database with an event store and fully decouple the components by using a message queue.
Of course, the magnitude of this presents a problem: I can either merely show these examples as diagrams, leaving the hard questions about how this can actually work to you, the reader. Or I can provide — reasonably simple, but complex enough to make the principles show — demo implementations, which you can run, test and compare.
I have chosen to do the latter. You will find all the referenced code at Github
In order to be able to do this, I had to omit such “minor” details as security, user and session management, not to mention the actual payment. Please be kind – it should still serve as a decent example.
5. Implementation #1: A Modular Monolith
The first stage in our incremental design evolution is basically how folks should design a properly modularized monolithic application. And to make it clear – this is absolutely a valid design for many contemporary web applications! Monoliths get far too little credit: They usually can be kept running and bug-free at reasonable cost, have a fairly small resource footprint, and their deployment and operations can be easily managed by a single team.
For a very large percentage of businesses — I will go out on a limb here and make a bet — this is absolutely sufficient. Most companies don’t need elaborate microservice architectures and global scale — in fact, none of them do, when they start their businesses: On the contrary, they will greatly benefit from keeping things simple, until demand grows too quickly and it’s no longer enough (which is arguably the best problem to have, when you’re a growing business).
The basic structure can be pictured like this:
Our first implementation contains two frontend apps made with React (shop-gui and admin-gui), and a single server (example-shop), which uses SpringBoot for the backend, and a MySQL database. The domain core is included as a library (domain-core), which does not rely on any external dependencies, except for lombok (to allow for less source code) and joda-money (because we don’t want to implement currency handling ourselves).
This is the key benefit of Hexagonal Architecture: We get to keep our core business entirely framework-agnostic, ensuring that it will only ever have to change when the actual business changes.
This isolation is realized by providing an
api package, which contains interfaces for the incoming (e.g.
OrdersApi) and the outgoing adapters (
OrdersRepository), as well as the serializable data objects (
Order) to be passed to and from the core. The rule is: No outside objects may be used within the core, and we never pass objects containing the business logic (
impl) to external classes. This necessitates some extra mapping (see
ShoppingCartFactory), but prevents the actual use of business methods, and the pollution of our domain with implementation details.
Our business rules have been implemented as domain services (e.g.
OrdersCheckoutPolicyService), which call the APIs internally. and we chose to do the same for our validation problem:
ProductValidationService will throw an error, if an added shopping cart item is not a valid product.
The core was developed using TDD, and is hence fully unit tested. Note that we did not use any mocking tools, but rather implemented a simple
RepositoryInMemory to use during testing. The tests are very fast and give a complete documentation of our business logic.
In the Spring module, we replace our
RepositoryJpa, and within it, we use the well known
CrudRepository from Spring Data. Note that the JPA entity structure is not identical to the business entity structure used within the domain core, and we don’t use lazy loading, or relational annotations, such as
@OneToMany, at all.
We have found that keeping our JPA structures simple, and combining/separating/calculating data using Java streams works well for us, but we may change this in the future.
The finished product is deployed into docker containers behind a load balancer, which makes everything appear, as though it were a single service:
Continued in Part 2 .
Special Thanks to Stefan Herrmann and Alexander Rose for helping to sort out both the language and content of this blog post, and for fixing some of my uglier bugs in the example repository 🙂
- The term was first mentioned in Edsger W. Dijkstra’s paper “On the Role of Scientific Thought” , published in 1974 – but the concept itself must have been around even earlier. ↩︎
- Evans, Eric (Addison-Wesley, 2004): Domain-Driven Design: Tackling Complexity in the Heart of Software ↩︎
- Emphasis added by me. ↩︎
- The application layer is explicitly “kept thin”, meaning it should contain only the task/controller logic for a specific use case (such as the sequence of steps a user needs to perform in a longer running process), but not the business data and logic itself. ↩︎
- Fowler, Martin (Addison-Wesley, 2002): “Patterns of Enterprise Application Architecture” ↩︎
- Cockburn, Alistair (2000, Addison-Wesley): “Writing Effective Use Cases” ↩︎
- The hexagonal shape (and name) is used mostly to distinguish the approach from a layered style. There is no connection to the number six, nor a need to have this exact number of ports and adapters. ↩︎
Hexagon, Schmexagon? – Part 2
Exploring Variations of Implementing Domain Driven Design With The “Ports and Adapters” Pattern, Part 2 Hexagonal Architecture is a key design pattern to use when implementing Domain Driven Design. It enables evolutionary changes, helps to keep test ...
- Software architecture
- Software development
30.7.2020 | 8 Minuten Lesezeit
Writing Better Tests With JUnit
TLDR; Writing readable tests is at least as important as writing readable production code. But the standard JUnit tooling won’t help us. In order to create a readable, maintainable, useful test suite, we need to change our testing habits. The focus ...
5.1.2016 | 17 Minuten Lesezeit
A Cultural Divide – Why The Hell Are We So Stubborn?
“The only thing that is constant is change.” – Heraclitus Bonfire of the Vanities Over the last few months, there have been quite a few clamorous controversies in the global programming community, driven by diametrically opposing views on fundamental...
- Software architecture
- Software development
4.8.2014 | 10 Minuten Lesezeit
We Suck At This
The views and opinions expressed in this commentary are solely those of the author. 1. Let’s be frank We suck at being open and welcoming towards women. We also suck at being open and welcoming towards minorities. All of us, the entire software industry...
6.6.2014 | 10 Minuten Lesezeit
Reflections on Curly Braces – Apple’s SSL Bug and What We Should Learn...
Everyone’s shaking their heads First of all, I assume that by now, everyone who has ever read a single tweet in his/her life has heard about Apple’s instantly infamous “gotofail” bug by now, and most of you have probably already read Imperial Violet’...
27.2.2014 | 11 Minuten Lesezeit
Dein Job bei codecentric?
Agile Developer & Consultant (w/d/m)
An allen Standorten
More articles in this subject area\n
Discover exciting further topics and let the codecentric world inspire you.
Hotwire: Ein neuer (alter) Ansatz für moderne Webanwendungen
24.8.2022 | 9 Minuten Lesezeit
Ein Microservice mit Kotlin und Ktor – ohne Spring
Ktor (s. https://ktor.io/ ) ist ein Framework für Kotlin, das sowohl Client- als auch Serverfunktionen bereitstellt und sich vorrangig der Kotlin DSL anstelle von Annotations bedient.Vor einiger Zeit (2018 war doch erst gestern?…) hat sich Lovis dieses...
14.6.2022 | 4 Minuten Lesezeit
Serverless Java mit AWS – Zwei Jahre Cloud-Native
Vor zwei Jahren haben wir angefangen, ein Kundenprodukt Cloud-Native auf Basis von Serverless, Java und AWS Managed Services umzusetzen. Im Folgenden möchte ich beschreiben, was wir in dieser Zeit gemeinsam gelernt haben und was wir heute besser machen...
2.12.2020 | 9 Minuten Lesezeit
Kong API-Gateway – Observability mit Prometheus, Grafana und OpsGenie
Im vorherigen Blogpost habe ich das bestehende Demo-Setup um decK und Konga erweitert. Nun soll es darum gehen, die vorhandenen Daten der APIs sichtbarer werden zu lassen. Hierzu möchte ich zwei Observability Patterns, nämlich Monitoring und Alerting...
- Open Source
19.12.2019 | 4 Minuten Lesezeit
Kong API Gateway – Deklarative Konfiguration mit decK und Visualisierung...
Seit dem letzten Post ist eine neue Version (1.4 ) des Kong API Gateways veröffentlicht worden. Die größte Neuerung stellt die /status-Route dar. Über diese lässt sich der Status eines Gateways direkt abfragen. Anfang Dezember ist auch ein Patch-Release...
- Open Source
12.12.2019 | 4 Minuten Lesezeit
REST: Standardisierte Fehlermeldungen mittels RFC 7807 Problem Details
REST-Fehlermeldungen: Einleitung Wenn man eine REST-Schnittstelle implementiert, kommt schnell die Frage auf, wie man Fehler am besten zurückgibt. Die erste und naheliegendste Option sind die HTTP-Statuscodes (4xx, 5xx je nach Problem) – diese sind ....
- Open Source
10.9.2019 | 5 Minuten Lesezeit
API-Management mit Kong – Ein Update und mehr
Seit dem letzten Blogpost zu diesem Thema von Alexander Melnyk sind fast zwei Jahre vergangen, und es ist in Sachen „API-Management mit Kong“ eine Menge passiert. Daher war es an der Zeit, zum einen die Inhalte des Posts von Alexander zu aktualisieren...
- Open Source
3.9.2019 | 5 Minuten Lesezeit
Event Storming – Gemeinsam die Domäne entdecken
In der Praxis treten innerhalb von Projekten häufig Schwierigkeiten auf, die auf unklare oder unscharfe Abgrenzung von Fachlogik zurückzuführen sind. Ursache häufig: die Problemdomäne ist für den Kunden nicht immer klar, oder von vornherein wurde die...
13.8.2019 | 11 Minuten Lesezeit
AWS lokal entwickeln mit Serverless Framework Offline Plugins
Wer mit dem Serverless Framework auf AWS entwickelt, kann dies mit ein wenig Aufwand auch lokal auf dem eigenen Rechner tun. Hierfür gibt es eine Vielzahl verfügbarer Plugins, um Services wie z. B. Lambdas, DynamoDB oder S3 lokal zu nutzen. Wie ihr...
4.8.2019 | 5 Minuten Lesezeit
Schnelles Entwickeln mit Kubernetes in Azure
Kubernetes ist die de facto Deployment-Umgebung für moderne Microservice-Architekturen. Alle großen Cloud-Anbieter haben daher Angebote für Kubernetes, die durch zahlreiche Features ergänzt werden, die Ressourcen des jeweiligen Anbieters intelligent ...
31.7.2019 | 5 Minuten Lesezeit
RESTful Webservices mit Quarkus
Im ersten Artikel zu Quarkus wurde beschrieben, wie man es nutzen kann und was die theoretischen Hintergründe sind. In diesem Artikel wird beleuchtet, wie mit Quarkus eine vollständige REST-Anwendung erstellt werden kann. In der Anwendung werden verschiedene...
3.6.2019 | 7 Minuten Lesezeit
Quarkus macht Java fit für die Cloud
Vor über zwanzig Jahren wurde Java vorgestellt, und es ist bis heute eine der erfolgreichsten Programmiersprachen. Durch sein Alter ist Java jedoch nicht auf die Cloud optimiert und fällt hier hinter anderen Sprachen zurück. Java ist eher auf ein monolithisches...
9.4.2019 | 7 Minuten Lesezeit
Abweichungen zwischen Spezifikation und REST-API mit hikaku erkennen
Wenn man eine REST-API mit dem Contract-first-Ansatz erstellt, verwendet man vermutlich Codegenerierung oder einen anderen Weg, um sicherzustellen, dass die Spezifikation und die Implementierung im Laufe der Zeit inhaltlich gleich bleiben. In diesem ...
- Open Source
8.3.2019 | 3 Minuten Lesezeit
Domain-driven API-first Design mit Schema.org
APIs sind längst mehr als technische Schnittstellen zwischen Backend und Frontend. Für viele Unternehmen werden APIs zum Kern des Geschäftsmodells. Unter den Stichworten „API as a Product“ und „API-first“ rücken disruptive Unternehmen APIs in den Fokus...
5.3.2019 | 7 Minuten Lesezeit
Shared Code in Microservices
Microservices sind schon lange kein Hype-Thema mehr. Unzählige Blogartikel, Bücher, Best Practices, Tweets und War Stories aus konkreten Projekten zeugen von einem gelebten Architektur-Stil. Es gibt kaum eine Frage, die nicht bereits mehrfach von allen...
16.7.2018 | 10 Minuten Lesezeit
Einführung in OpenTracing
In diesem Beitrag möchte ich einen ersten Einstieg in das Thema OpenTracing geben.Egal, ob man ein monolithisches System modernisieren möchte oder auf der grünen Wiese startet – aktuell ist der Trend ganz klar eine Service- oder Microservice-Architektur...
- Open Source
1.2.2018 | 9 Minuten Lesezeit
API-Management mit Kong
+++Update: Die Inhalte dieses Blog Posts beziehen sich auf eine inzwischen teils veraltete Version von Kong+++Die Verwendung von APIs zur Förderung von Innovationen und zur Schaffung neuer Geschäftsmöglichkeiten ist kein neues Konzept. Viele Success ...
23.11.2017 | 10 Minuten Lesezeit
Keycloak und Spring Security Teil 1: Einrichtung & Frontend
Wie lässt sich Keycloak als Identity- und Accessmangement-Provider in aktuelle Frameworks, speziell verteilte Spring Boot-Services, einbinden? Dazu am besten noch so in Spring Security integrieren, dass man nicht alles neu-erlernen oder umschreiben muss...
3.9.2017 | 8 Minuten Lesezeit
Ansible zaubert Spring Boot Apps auch auf Windows
Manchmal kommt es tatsächlich vor, dass wir unsere Spring Boot Apps auf Windows ausführen müssen, anstatt wie gewohnt auf Linux. Das passiert insbesondere dann, wenn wir native Bibliotheken aufrufen müssen, die Windows voraussetzen. Trotzdem sollten ...
- Infrastructure as Code
19.7.2017 | 14 Minuten Lesezeit
Scala und Spring Boot – geht das gut?
Scala ist eine der populärsten alternativen Programmiersprachen für die JVM. Funktionale Programmierung, Typinferenz, eine mächtige Collections-Bibliothek und asynchrone und parallele Ausführung sind Kernmerkmale dieser Sprache. Sie hat sich insbesondere...
4.7.2017 | 8 Minuten Lesezeit
Gemeinsam bessere Projekte umsetzen
Wir helfen Deinem Unternehmen
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Do you still have questions? Just send me a message.