SummerSoC 2020: Domain-driven Service Design


SummerSoC Logo

At SummerSoC 2020 I present(ed) my (Stefan Kapferer) and Olaf Zimmermann’s paper on «Domain-driven Service Design» (accepted; to be published soon)

You can download the slides here.

The paper covers the following topics:

  • Service decomposition with Strategic Domain-driven Design (DDD)
  • Context Mapper DSL (CML) as a machine-readable approach to DDD Context Mapping
  • Decomposition criteria: How to decompose a domain and identify «Bounded Contexts»?
  • Architectural Refactorings (ARs) as model transformation for CML
  • An incremental method to decompose domains «step by step»
  • Service contract generation out of the DDD models (with the MDSL language)

Since its not possible to cover all the topics in a 20 minutes presentation, I focus(ed) on Strategic DDD, the Context Mapper DSL (CML), and the incremental decomposition method with Architectural Refactorings (ARs). In this blogpost I provide CML code for the examples I use(d) and continue the process towards service contract generation with the Microservice Domain-specific Language (MDSL) (developed by Olaf Zimmermann).

Content Outline

  1. Strategic Domain-driven Design and Context Mapper
  2. Decomposition Criteria
  3. Architectural Refactorings (ARs) as Model Transformations
  4. Incremental Service Decomposition
  5. Service Contract Generation
  6. Wrap Up

Strategic Domain-driven Design and Context Mapper

The question how to decompose a software system into modules and/or services has gained much attention with the recent Microservice trend. The question is however not new. Parnas already wrote a paper «On the criteria to be used in decomposing systems into modules» in 1972.

One popular answer these days is Strategic Domain-driven Design (DDD). It emphasizes the need for a distinctive vocabulary, the ubiquitous language, in the domain model and suggests to decompose a domain into so-called «Bounded Contexts». A domain typically consists of multiple subdomains. In our presentation example: the insurance domain consists of subdomains such as customers, policies, risks, etc.

DDD Subdomains - a fictitious insurance scenario

When implementing such a system with a service-oriented architectural style, microservices, or a modular monolith («modulith»), you are confronted with the question how many (micro-)services or modules the system shall exhibit. DDD practitioners suggest to identify Bounded Contexts and implement one subsystem (service or module) per context. A Bounded Context establishes a boundary within which a particular model is valid.

The concepts and domain objects inside a Bounded Context shall be defined clearly and distinctively (the ubiquitous language). As the following example illustrates, a Bounded Context can implement parts of one or multiple subdomains:

DDD Bounded Contexts implement multiple subdomains

When we decompose our domain into multiple Bounded Contexts the question how this contexts integrate with each other raises naturally. This is where DDD Context Maps and context mapping come into play. A Context Map illustrates the relationships between Bounded Contexts and how the data between them flow. The following Context Map shows one example for our fictitious insurance scenario:

Example Context Map

The «U» and «D» at some of the relations mean «upstream» and «downstream». The data in «Upstream-Downstream» relationships always flow from the upstream to the downstream. That means: the upstream exposes a part of its domain model (for example via a public API) to the downstream. The downstream context can either conform to the upstreams model (CONFORMIST), or implement an Anticorruption Layer (ACL) to protect its own model from upstream changes. Upstream patterns are Open Host Service (OHS) and Published Language (PL). The OHS pattern is applied if the upstream context implements a public and open API that is supposed to be used by multiple downstream context in a unified manner. The published language (PL) is the part of the domain model that is exposed by the upstream - which is why this pattern is often used in combination with OHS. If a relationship is marked with «Customer/Supplier», the two teams work closely together in a sense that the upstream team prioritizes its work according to the downstream (customer) requirements. In symmetric relationships (Partnership and Shared Kernel) there exist interdependecies which lead to a stronger coupling in comparison to Upstream-Downstream relationships. For more details about the DDD patterns, we recommend the following books and resources:

Context Mapper offers a Domain-specific Language (DSL) to model DDD Context Maps and Bounded Contexts in a machine-readable manner. DDD practitioners have drawn Context Maps manually on paper so far. One of our goals with Context Mapper was to reduce ambiguities regarding strategic DDD patterns and provide a clear and precise interpretation of how the patterns can be combined. We further believe that DDD Context Maps are artifacts that must evolve iteratively. The DSL approach comes with a few benefits:

  • Precise definition of what can be combined (semantic language rules/validators)
  • Models can be processed and transformed
    • Allows the implementation of model transformations/refactorings
  • Possibility to generate other representations of the model
    • Currently:
      • PlantUML component and class diagrams
      • Graphical Context Maps (as shown above)
      • Service contracts (MDSL)
      • Microservice code (via JHipster)

The fictitious insurance example Context Map (as illustrated above) can be written in Context Mapper DSL (CML) as follows:

ContextMap InsuranceContextMap {
  /* Add bounded contexts to this context map: */
  contains CustomerManagementContext, CustomerSelfServiceContext, PrintingContext
  contains PolicyManagementContext, RiskManagementContext, DebtCollection
  
  /* Define the context relationships: */ 
  CustomerSelfServiceContext [D,C]<-[U,S] CustomerManagementContext
  
  CustomerManagementContext [D,ACL]<-[U,OHS,PL] PrintingContext
  
  PrintingContext [U,OHS,PL]->[D,ACL] PolicyManagementContext
  
  RiskManagementContext [P]<->[P] PolicyManagementContext

  PolicyManagementContext [D,CF]<-[U,OHS,PL] CustomerManagementContext

  DebtCollection [D,ACL]<-[U,OHS,PL] PrintingContext

  PolicyManagementContext [SK]<->[SK] DebtCollection  
}

/* Bounded Context definitions */
BoundedContext CustomerManagementContext implements CustomerManagementDomain {
    /* can contain domain model based on tactic DDD patterns */
}

BoundedContext CustomerSelfServiceContext implements CustomerManagementDomain

BoundedContext PrintingContext implements PrintingDomain

BoundedContext PolicyManagementContext implements PolicyManagementDomain

BoundedContext RiskManagementContext implements RiskManagementDomain

BoundedContext DebtCollection implements DebtsDomain

/* domain & subdomain definitions removed to save space */

Note: Context Mapper not only supports modeling Bounded Contexts, you can also model your domain with its subdomains and specify which Bounded Contexts implement which subdomains. In addition, you can start your modeling process by declaring use cases or user stories and derive your subdomains and Bounded Contexts automatically. I don’t explain this process here, as my colleague Olaf Zimmermann already covers this in his blogpost featuring our rapid prototyping transformations.

For more information and starting points for Context Mapper I recommend the following links:

Decomposition Criteria

We have now seen how we can model DDD Context Maps and therefore describe system architectures in terms of Bounded Contexts. But this does still not really answer the question how we can identify Bounded Contexts. Which criteria and heuristics can we use to do that?

We distilled a set of «Decomposition Criteria» (DCs) empirically. We used research papers (such as the one of D.L. Parnas «On the Criteria To Be Used in Decomposing Systems into Modules»), resources of DDD experts (books, blogposts, conference talks), and our own experience in software projects. Here just a few of often mentioned decomposition criteria:

  • Use Cases
  • Ownership and teams (Conway’s law)
  • Language and domain expert boundaries
  • Business process steps
  • Business capabilities
  • Data flow
  • Non-functional requirements (NFRs) such as security, availability, etc.

These are all criteria that can be reasons to split a Bounded Context or extract parts of a domain model into a separate Bounded Context. The InfoQ article on Context Mapping by Alberto Brandolini illustrates how such criteria can be used to identify Bounded Contexts and decompose an initial domain «step by step».

Architectural Refactorings (ARs) as Model Transformations

One of the advantages of having a machine-readable Context Map: we can implement model refactorings for our DSL. Thus, we can implement refactorings that allow users to decompose the described domain with tool-support.

Based on the decomposition criteria mentioned above we proposed a set of «Architectural Refactorings (ARs)»:

Architectural Refactorings (ARs) Overview

One example: «Split Bounded Context by Owner (AR-3)» ensures that only one team works on one Bounded Context. It can be applied if multiple teams work on the same context. The result of the refactoring: one Bounded Context per team. This increases team autonomy and establishes clear responsibilities.

We implemented this ARs as model transformations/refactorings for the CML language. You find an overview over all implemented ARs here. In the following I illustrate the application of «Split Bounded Context by Owner (AR-3)» on a CML model (as in my SummerSoC presentation).

Assume we have the following Bounded Context in our CML model (I still use the fictitious insurance scenario):

/* Bounded Context Definitions */
BoundedContext CustomerManagementContext {
  type = FEATURE
  domainVisionStatement = "The customer management context is responsible for managing all the data of the insurance companies customers."
  implementationTechnology = "Java, JEE Application"
  responsibilities = "Customers, Addresses"

  Aggregate CustomersMainAggregate {
    owner CustomerBackendTeam
    
    Entity Customer { 
      aggregateRoot
      
      - SocialInsuranceNumber sin
      String firstname
      String lastname
      - List<Address> addresses
    }
    
    Entity Address {
      String street
      int postalCode
      String city
    }
    
    ValueObject SocialInsuranceNumber {
      String sin key
    }
  }

  Aggregate CustomerSelfServiceAggregate {
        owner CustomerFrontendTeam

        Entity UserAccount {      
      String username
      - Customer accountCustomer
    }
    Entity CustomerAddressChanged {
      aggregateRoot
      
      - UserAccount issuer
      - Address changedAddress
    }
  }
    
}

/* Team's: */
BoundedContext CustomerFrontendTeam { type TEAM }
BoundedContext CustomerBackendTeam { type TEAM }

The given Bounded Context contains two Aggregates. With the owner keyword, CML allows to assign the team that owns a given Aggregate. A team is a Bounded Context as well (in CML modeled with type TEAM). This describes a situation where teams and system or feature Bounded Contexts are not aligned. Multiple teams work on the same Bounded Context. In the Context Mapper IDE (VS Code extension, Eclipse Plugin, or online) we can not apply «Split Bounded Context by Owner» on this context:

Application of Architectural Refactoring in Visual Studio Code (Screenshot)

For the model given above the refactoring will produce the following output:

/* Bounded Context Definitions */
BoundedContext CustomerManagementContext {
  domainVisionStatement = "The customer management context is responsible for managing all the data of the insurance companies customers."
  type FEATURE
  responsibilities "Customers, Addresses"
  implementationTechnology "Java, JEE Application"
  
  Aggregate CustomerSelfServiceAggregate {
    owner CustomerFrontendTeam
    Entity UserAccount {
      String username
      - Customer accountCustomer
    }
    Entity CustomerAddressChanged {
      aggregateRoot
      - UserAccount issuer
      - Address changedAddress
    }
  }
}

BoundedContext NewBoundedContext1 {
  Aggregate CustomersMainAggregate {
    owner CustomerBackendTeam
    Entity Customer {
      aggregateRoot
      - SocialInsuranceNumber sin
      String firstname
      String lastname
      - List<Address> addresses
    }
    Entity Address {
      String street
      int postalCode
      String city
    }
    ValueObject SocialInsuranceNumber {
      String sin key
    }
  }
}

/* Team's: */
BoundedContext CustomerFrontendTeam {
  type TEAM
}

BoundedContext CustomerBackendTeam {
  type TEAM
}

As you can see, the resulting model contains one Bounded Context per team. By using the «Rename» refactoring we can improve the naming of the given Bounded Contexts: (we also post-processed the Bounded Context attributes a bit)

/* Bounded Context Definitions */
BoundedContext CustomerSelfServiceContext {
  domainVisionStatement = "The customer self-service context provides a web application where customers can change their address."
  type FEATURE
  responsibilities "Handle address change requests of customers"
  implementationTechnology "React Web Application"
  
  Aggregate CustomerSelfServiceAggregate {
    owner CustomerFrontendTeam
    Entity UserAccount {
      String username
      - Customer accountCustomer
    }
    Entity CustomerAddressChanged {
      aggregateRoot
      - UserAccount issuer
      - Address changedAddress
    }
  }
}

BoundedContext CustomerManagementContext {
  domainVisionStatement = "The customer management context is responsible for managing all the data of the insurance companies customers."
  type FEATURE
  responsibilities "Customers, Addresses"
  implementationTechnology "Java, JEE Application"

  Aggregate CustomersMainAggregate {
    owner CustomerBackendTeam
    Entity Customer {
      aggregateRoot
      - SocialInsuranceNumber sin
      String firstname
      String lastname
      - List<Address> addresses
    }
    Entity Address {
      String street
      int postalCode
      String city
    }
    ValueObject SocialInsuranceNumber {
      String sin key
    }
  }
}

/* Team's: */
BoundedContext CustomerFrontendTeam {
  type TEAM
}

BoundedContext CustomerBackendTeam {
  type TEAM
}

Note: The refactorings also ensure that Context Map relationships (if you have any) stay consistent and are corrected if necessary (not shown above in order to keep the example shorter/simpler).

Incremental Service Decomposition

In the refactoring example above we have shown one single decomposition step. We believe that decomposing a software system or domain into a Context Map (multiple Bounded Contexts) is an iterative and evolutionary process. By providing multiple ARs that are based on different decomposition criteria, one can decompose a domain with Context Mapper «step by step». The following graphic illustrates this idea:

Incremental Decomposition Process

Users can start with the domain analysis and model their domain as one single context first. By applying the ARs on the strategic DDD level (see image above), users can decompose the domain «step by step» and evolve a Context Map iteratively. We also offer refactorings on the tactic DDD level that allow to decompose and merge Aggregates inside a Bounded Context.

Note: Have a look into our rapid prototyping tutorial and Olaf Zimmermann’s blogpost to learn how you can derive Bounded Contexts from subdomains automatically. These model transformations ease the shift from the «domain analysis» phase into the «Stratgic DDD» phase as illustrated above. Once you derived a Bounded Context from one or multiple subdomains, you can decompose it by using the ARs.

Service Contract Generation

As soon as you prototyped your Context Map, the API’s that integrate your subsystems (Bounded Contexts) become an important design issue. How shall they be designed/implemented and which parts of your domain models are exposed to other Bounded Contexts? Context Mapper offers you the possibility to generate MDSL contracts out of your CML Context Map. By using the MDSL tools you can even generate technology-specific contracts and code to rapidly prototype your applications later.

In CML you can declare which Aggregates are exposed in a Context Map relationship. Have a look at the following example:

/* Example Context Map written with 'ContextMapper DSL' */
ContextMap InsuranceContextMap {
  type = SYSTEM_LANDSCAPE
  state = TO_BE

  contains CustomerManagementContext, CustomerSelfServiceContext

  CustomerManagementContext [OHS, PL]->[ACL] CustomerSelfServiceContext {
    exposedAggregates CustomersMainAggregate
    implementationTechnology "RESTful HTTP"
  }
}

/* Bounded Context Definitions */
BoundedContext CustomerSelfServiceContext {
  domainVisionStatement = "The customer self-service context provides a web application where customers can change their address."
  type FEATURE
  responsibilities "Handle address change requests of customers"
  implementationTechnology "React Web Application"
  
  Aggregate CustomerSelfServiceAggregate {
    Entity UserAccount {
      String username
      - Customer accountCustomer
    }
    DomainEvent CustomerAddressChanged {
      aggregateRoot
      - UserAccount issuer
      - Address changedAddress
    }
  }
}

BoundedContext CustomerManagementContext {
  domainVisionStatement = "The customer management context is responsible for managing all the data of the insurance companies customers."
  type FEATURE
  responsibilities "Customers, Addresses"
  implementationTechnology "Java, JEE Application"

  Aggregate CustomersMainAggregate {
    Entity Customer {
      aggregateRoot
      - SocialInsuranceNumber sin
      String firstname
      String lastname
      - List<Address> addresses
    }
    Entity Address {
      String street
      int postalCode
      String city
    }
    ValueObject SocialInsuranceNumber {
      String sin key
    }
    /* added service operation */
    Service AddressService {
      boolean changeAddress(@Address address);
    }
  }
}

Note: In order to be able to generate contracts, an exposed Aggregate must at least contain one operation on the «Root Entity» or within a Service.

In the CML example above we used the two Bounded Contexts that we decomposed earlier in this blogpost. We have a customer management context and a self-service context that offers a web interface so that users can change their address by themselves. We further added a Context Map with a relationship between the contexts. Concretely: the self-service context needs the management context to change addresses. We added a service method changeAddress in the management context accordingly. The Aggregate that contains the service is marked as exposed with the exposedAggregates keyword in the Context Map relationship.

Based on this information we can now generate an MDSL contract in Context Mapper:

MDSL Generation in Visual Studio Code (Screenshot)

The generator produces the following simple MDSL contract:

// Generated from DDD Context Map 'blog-demo.cml' at 12.09.2020 16:04:15 CEST.
API description CustomerManagementContextAPI
usage context PUBLIC_API for BACKEND_INTEGRATION and FRONTEND_INTEGRATION

data type Address { "street":D<string>, "postalCode":D<int>, "city":D<string> }

endpoint type CustomersMainAggregate
  exposes
    operation changeAddress
      expecting
        payload Address
      delivering
        payload D<bool>


// Generated from DDD upstream Bounded Context 'CustomerManagementContext' implementing OPEN_HOST_SERVICE (OHS) and PUBLISHED_LANGUAGE (PL).
API provider CustomerManagementContextProvider
  // The customer management context is responsible for managing all the data of the insurance companies customers.
  offers CustomersMainAggregate
  at endpoint location "http://localhost:8001"
    via protocol "RESTful HTTP"


// Generated from DDD downstream Bounded Context 'CustomerSelfServiceContext' implementing ANTICORRUPTION_LAYER (ACL).
API client CustomerSelfServiceContextClient
  // The customer self-service context provides a web application where customers can change their address.
  consumes CustomersMainAggregate

The contract contains one endpoint type with the changeAddress operation and one data type to represent the address. In addition, it contains one API provider that offers the endpoint and one API client that consumes it.

For more MDSL examples and an introduction into the language I refer to the MDSL website. Olaf Zimmermann’s blogpost further shows how you can use the MDSL contract to generate technology-specific contracts such as Open API Specifications (OAS); see step 7 in his tutorial.

Wrap Up

With the generation of MDSL contracts I am done illustrating our SummerSoC 2020 paper contributions using a fictitious example model.

In this post I gave a short introduction into service decomposition with strategic Domain-driven Design (DDD), Context Mapper with its Architectural Refactorings (ARs), our proposed incremental decomposition method, and contract generation with MDSL.

Please keep in mind that it is our goal to support domain/service modelers, software architects and software engineers by offering helpful tools that ease applying strategic DDD in projects. It is not our goal to replace the human work! The tools can hopefully support you, but you still need the people knowing your domain and the experience of modelers and architects to design good domain models and API’s.

Do you have comments? Suggestions? You miss some features in Context Mapper? Please contact me! I always appreciate feedback.

I hope you will give Context Mapper a try ;)

Stefan

Acknowledgements

This blogpost, the SummerSoC 2020 paper, and Context Mapper would not exist without Olaf Zimmermann. Thank you very much for the great collaboration, teamwork, and all your support!

UPDATE: SummerSoC Young Researcher Award 2020

Many thanks to the organizers of SummerSoC 2020 and ServTech for selecting me to receive the «SummerSoC Young Researcher Award 2020» 🙏

SummerSoC Young Researcher Award 2020 Picture (Stefan Kapferer)

It was a pleasure to be part of this event and of course I feel honored to receive this award!

Stefan