Nov 21, 2019

Tackling Business Complexity with Strategic Domain-Driven Design

Careers Team
Open roles
No items found.
View all open roles

Leandro Lages, engineering manager, explains how teams can benefit from strategic domain-driven design (DDD). Focusing on strategic patterns, he shares how his Discovery team deals with the many upstream systems, and how understanding each pattern can enable you to design the best possible integrations.


Most software engineers love building systems from scratch or working in greenfields products using state of the art technology or framework. So it’s natural that, as the system and product evolves, the business logic and problems to be solved get increasingly more complex.

As this accidental complexity continues to sprawl, the architecture and source code also gets more complicated as new features are built, and new teams are created within the organization. As we continue to grow as a team, there’s always a need to touch source code created by someone else. More often than not, it’s legacy code.

Often, engineers may have to deal with a part of the code that works, and the knowledge around it is so sparse since that part isn’t maintained by anyone anymore. Over time, you’ll find any one of these scenarios in companies that experience exponential growth.

Why you should consider DDD

Domain-driven design as a better alternative

What’s interesting about DDD, is its nature of tackling all these levels of intricacy, not only at the source code level but also when it comes to social architecture and organizational design.

Aligning this solution space with the reality of your problem space is the primary focus of DDD.

DDD takes a holistic approach to complexity by aligning domain experts on the problem space and engineers who will implement the solution space. Having a ubiquitous language between both problem and solution space is also a defining characteristic of DDD.

Problem space and solution space keeping its individual characteristics and still using the same language

In 2003, Eric Evans introduced the concept of DDD. Since then, the DDD community has evolved, and new ideas and patterns have unfolded.  

For example, let’s look at event storming and domain storytelling for the problem space. In the solution space, we basically have two main groups of patterns: The tactical patterns, which are more focused on the source code, and the strategic patterns, which are focused more on architecture and how organizations can be designed in order to tackle the complexity.

In today’s post, we’re going to focus on strategic patterns. There are many engineers at GetYourGuide who are interested in this topic, especially the ones in the Discovery team. Across departments, we have teams that focus on specific missions. Discovery is made up of UX team members, a product manager and designer, and engineers. Our team deals with many upstream systems, and understanding each strategic pattern enables us to design the best possible integration with them.

You might also be interested in: The framework to excel as an Engineering Manager

Strategic domain-driven design with bounded context and context mapping  

Strategic patterns are focused on integrating bounded contexts, which are logical boundaries in your system. When the problem space is defined with domains and subdomains, it's time to implement the architecture/teams that will take care of the solution space. Bounded contexts define a common language or model within a subdomain. Usually, when the language or model begins changing its meaning, it means that another bounded context can be created.

One example at GetYourGuide demonstrates the concept of an Activity. From the supplier point of view, an Activity is a model with many fields like title, description, highlights, meeting point, different starting times, categories, and so on. From a search point of view, an Activity can mean just the title, a "from price", an image, and reviews rate.

To visualize how different bounded context interact, DDD has a general-purpose technique/tool called context mapping. When creating a context map, it's possible to identify potential logical boundaries in the system. Strategic patterns work by specifically integrating those different bounded contexts.

An example of context mapping

Strategic patterns at a glance

To exemplify the patterns in this section and their advantages and trade-offs, we can look at different subdomains that exist in our current marketplace:

  • Catalog
  • Inventory
  • Discovery
  • Activity Booking

From a supplier domain, we have Catalog and Inventory subdomains. Catalog is responsible for the structure of all static data from the activities we sell like title, image, meeting point, and different options like the language of the tours, starting times, and so on.

Once we have the Catalog data saved, another subdomain is responsible for making sure we have availability and inventory to sell with the right prices. All updates on the availability and prices are also the responsibility of the Inventory subdomain. This can happen by integrating directly with suppliers via APIs, inbound or outbound, or updating the inventory and price directly from the supplier portal.

From a traveler domain, we have Discovery and Activity Booking subdomains. Discovery is responsible for providing a smooth experience for the users to find what they want through search and/or recommendation of activities. Once they find what they want, travelers can see the details of the activity and book them, beginning the checkout process.

You might also be interested in: A backend engineer highlighted on Talent Berlin.

Pattern: Shared kernel

When different bounded contexts need to share a common model, you have a shared kernel. This pattern usually happens in two bounded contexts of the same subdomain. Sometimes it’s in the form of a library or component. The main benefit here is to avoid code duplication of the respective bounded contexts. The trade-off is that the kernel itself needs to be managed by different teams and there is an effort in alignment about the requirements of this kernel. Usually, the shared kernel is not the core model of the respectives bounded contexts.

At GetYourGuide, an example would be the supplier profile in different bounded contexts within the supplier domain and the user profile within the different bounded contexts in the traveler’s domain. Another example would be the shared library to standardize the base models for events and experimentation in different bounded contexts of the traveler’s shopping platform.

The supplier model

Pattern: Customer-Supplier

When two different bounded contexts are in an upstream/downstream relationship, the upstream is the supplier, not to be confused with suppliers from GetYourGuide, and the downstream is the customer. More specifically, the changes needed in the upstream bounded context (supplier) usually are demanded by the customer. In this case, usually the customer is the only one demanding changes in the upstream. The trade-off of this pattern is its nature itself. Usually, in large organizations it’s very common to reuse the use cases from the upstream to attend demands of other possible downstream systems. In this case, there is an effort in place to keep all downstream use cases working.

A great example of this strategic pattern is the relation between our search page and the search API. The search API is the upstream system while the search page is the downstream e.g. controlled environment, features, backlog, and so on.

Two bounded contexts in a customer-supplier relationship

Pattern: Conformist

Conformist is also an upstream/downstream relationship between two bounded contexts. The difference is that the upstream system has no idea of the existence of the downstream and changes in this upstream are not demanded by the downstream, which means the downstream uses the upstream at its own risk. Usually this pattern is used in the short term for experimentation or when your organization has two teams that are far from each other in communication.

This pattern gets very interesting with an organization the size of ours. Let’s imagine the following example: We have a search page (UI) in a customer-supplier relationship with a search API. This means the client demands all changes in this API. Let’s also imagine that another team from another department — which is basically another subdomain/bounded context — wants to do an experiment, and they found out the existence of the search API. They realize that API would be enough for their experimentation or short term changes.

You might also be interested in: Systems health and how to create a DevOps culture

In this case, they start to conform with the search API and run their experiments. All good, as time passes, they may decide to still consume the search API. The problem is: The search UI is demanding changes in the search API, and the new changes over time break the contract with the external team conforming to it. A solution would be to transform the relation in a customer-supplier, which leads to improve communication and demand use cases to the upstream. As an alternative, go completely in separate ways and  build its own search — which would take some time. This is where trade-offs need to be analyzed and a decision made.

Example of a conformist relationship between two bounded contexts

Pattern: Anti-corruption Layer (ACL)

ACL is one of the most interesting and useful patterns in companies that experience rapid growth and have to deal with legacy systems. Here, there is also an upstream/downstream relation between two bounded contexts but at a lower level. The downstream creates a layer between itself and the upstream, to translate the models from the upstream into its own model, keeping its integrity. The main benefit is total independence from the upstream in defining the model in the downstream. As a trade-off, there is the effort to create the translation layer which will act as the ACL.


We're using this pattern in two situations:

  1. As a strategy to have our legacy as an API
  2. To index activities in a more read data model in ElasticSearch.

The first one allows us to create new models and APIs towards the future with less ambiguity and in a more testable way. In this case, we don't need to implement the whole logic behind the legacy now but create a translation layer that allows us to focus on the interface to move other contexts that depend on the current one.

You might also be interested in: Engineering Manager series part 6: systems health and how to create a DevOps culture

The second is a strategy to transform the whole activity model that we have in a more transactional way from Catalog and Inventory, to a model that better supports queries and discoverability in different bounded contexts of the shopping platform.

Use an anticorruption layer to integrate with code you don’t own or can’t change

Pattern: Open host service

Open host service is an evolution of the pattern above. Imagine that different teams started implementing an ACL. After some time, we realize that we have similar logic used in different bounded contexts generating duplicate code and sometimes ambiguity and confusion around business logic.

As an evolution, the upstream system can provide an open host service with the translation layer, or the downstream systems can partner to create one. The benefit is a centralized translation layer removing duplication of code. The trade-off is that it's not always possible to start with a correct open host service, or sometimes it's challenging to find the opportunity.

Our Discovery team is in the process of creating an open host for all bounded contexts existent in the shopping platform. We realized that different bounded contexts are implementing an ACL to check if activities are available in various touchpoints. There is duplication of code, no source of truth, and those different ACLs are implemented in a legacy system. Different bounded contexts like search UI, landing pages, wish lists, and so on, will use a centralized place to filter by availability that behind the scene is an ACL with the Inventory as an upstream system.

Multiple subsystems integrating with similar transformation efforts

Final Considerations

GetYourGuide has already begun the journey to exponential growth for the next few years. To make a sustainable growth in both engineering, product, and business creating the right social architecture is fundamental.

Problem and solution space aligned in a common language and an architecture that can incrementally accommodate change in order to move fast is the key to successful growth. That’s where strategic patterns play a role. By modeling our domain, establishing the right boundaries, and making the right choices in how they can be integrated, it will allow any change in the social architecture to be more cohesive.

Other articles from this series
No items found.

Featured roles

Marketing Executive
Full-time / Permanent
Marketing Executive
Full-time / Permanent
Marketing Executive
Full-time / Permanent

Join the journey.

Our 800+ strong team is changing the way millions experience the world, and you can help.

Keep up to date with the latest news

Oops! Something went wrong while submitting the form.