Top-Down View

Top-Down View

Event Storming

System Architecture

Context Map Patterns

CQRS Deep-Dive Example

More CQRS Hands-On

Questions

Microservices

Top-Down View

Exe / Jar / Zip

Objects

Classes

OOP

Design Patterns

Modules

Layers

Project

Developer cares about ...

Service

Exe / Jar / Zip

Objects

Classes

OOP

Design Patterns

Modules

Layers

Project

Tactical Design

Tactical Design

Strategic Design

Service

Service

Service

Sub-Domain

Sub-Domain

Domain

Domain-Driven-Design

Exe / Jar / Zip

Objects

Classes

OOP

Design Patterns

Modules

Layers

Project

Strategic Design Tools

Event Storming

What is Event Storming?

  • lightweight method to model a domain
    • within an event storming workshop we work in iterations to add layer by layer of elements to sharpen the picture of a domain
  • typical elements
    • ​​domain events, commands, aggregates
    • issues/hot spots, chances

    • external systems, read models, policies

    • users/actors, timers, loops

Why do we use Event Storming?

  • knowledge distribution
    • bring together people from different silos
    • domain experts/analysts/developers/architects
  • enforcing a sharp timeline
    • experts will create locally ordered sequence of events
    • trigger long-awaited conversation
  • following steps
    • structure will emerge
    • people and systems will be displayed
    • every layer sparkles a new type of conversation
    • ...

Domain Events

product added to cart
  • domain event (past tense)

  • orange sticky note

  • relevant for domain experts

Example

Command-Event-Pairs

  • command (present tense)

  • blue sticky note

  • triggers a domain event

product add to cart

Aggregates / Entities

  • aggregate/entity

  • yellow sticky note

  • data that is interacted with

product

Borders & Flow

  • find borders where a domain model has a different meaning

  • domain events flow between different models

Bounded Contexts

  • bounded context/sub-domain

  • red sticky note

  • core name that is relevant/valid

checkout

Example

Outcome / Big Picture

  • narrative/stories/user journey

  • line(s)

  • multiple storytellings

  • common language for everyone

Iterate / Color Puzzle Thinking

event
command
external system
policy
user
read model
user
command
  • add new colours/sticky notes: e.g. policies

  • challenging value (discover new opportunities, inconsistencies)

  • reveal pain points

  • enable cross-perspective conversation

System Architecture

provider-ui

storefront-ui

merchant-ui

Frontend

api proxy

tenant

auth

Core

business unit

site

shop

Shop Admin

Product

product

. . .

. . .

. . .

. . .

Bounded Contexts > Microservices

Each sub-domain is implemented as vertical:

  • represents a deployable unit (one or more)
  • is exclusively responsible for its services
  • owns its data
  • communicates with other verticals predominantly by using asynchronous messaging
  • provides a REST-API to leverage access to its function

Sub-Domains / Verticals

Ubiquitous Language

  • use the same vocabulary for the same concepts
  • e.g. domain experts, analysts, developers use same language to describe a problem on the whiteboard

Bounded Context

  • area of the problem space where the ubiquitous language is valid
  • e.g.
    • customer in sales = social interests, likes, needs
    • customer in support = history, tickets
    • customer in accounting = method of payment
    • customer in order = addresses, availability

Most important terms... I/II

Domain Model

  • interpretation of reality
  • includes the chosen aspects for implementation
  • refers to the abstraction of the subdomain

Most important terms... II/II

A domain model is to a bounded context

what classes are to objects

Context Map

  • relation between bounded contexts

Context Map Patterns

Upstream / Downstream

Call Flow
Model Flow

Context Map

Call Flow
Model Flow
Downstream System
Upstream System

Context Map Patterns Categorized

Model Flow

 Upstream Patterns

  • Open Host Service

  • Event Publisher

 Downstream Patterns

  • Customer / Supplier

  • Conformist

  • Anticorruption Layer

 In-Between Patterns

  • Shared Kernel

  • Published Language

  • Seperate Ways

Upstream System
Down-stream System

Context Map Patterns Categorized

Model Flow

 Upstream Patterns

  • Open Host Service

  • Event Publisher

 Downstream Patterns

  • Customer / Supplier

  • Conformist

  • Anticorruption Layer

 In-Between Patterns

  • Shared Kernel

  • Published Language

  • Seperate Ways

Upstream System
Down-stream System

Open Host Service

  • Bounded Context offers defined set of services that expose functionality

  • Any downstream system can implement their own integration

  • Especially useful with many systems that want to integrate

SOAP/REST
Upstream System
Down-stream System

Open Host Service

@RepositoryRestController
@RequiredArgsConstructor
public class SiteCustomController {

    @NonNull
    private final SiteService siteService;
    @NonNull
    private final EntityLinks entityLinks;

    @PostMapping("sites")
    public ResponseEntity<Resource<Site>> create(@Valid @RequestBody SiteWithAdminUser body) {
        SiteCreateResource siteBody = body.getSite();

        Site transientSite = Site.builder()
                .active(siteBody.isActive())
                .hostname(siteBody.getHostname())
                .locale(siteBody.getLocale())
                .build();
        Site site = siteService.create(transientSite, body.getAdminUser());

        return ResponseEntity
                .created(entityLinks.linkForSingleResource(Site.class, site.getId()).toUri())
                .body(new Resource<>(site));
    }

    @DeleteMapping("sites/{siteId}")
    public ResponseEntity<Void> delete(@PathVariable("siteId") Optional<Site> site) {

        site.ifPresent(siteService::delete);
        return ResponseEntity.noContent().build();
    }
}

Open Host Service

private const val SHOP_IMAGE_LOCATION_MAPPING = "/{imageId}"

@RepositoryRestController
@RequiredArgsConstructor
@RequestMapping(value = ["/shop/images"], produces = [HAL_JSON_VALUE, APPLICATION_JSON_VALUE])
class ImageController(
        private val tenantAccessor: TenantAccessor,
        private val imageMessagePublisher: ImageMessagePublisher,
        private val imageRepository: ImageRepository,
        private val imageService: ImageService,
        private val pagedResourcesAssembler: PagedResourcesAssembler<Image>
) {

    @PostMapping
    fun createImage(@Valid @RequestBody imageBody: Image): ResponseEntity<Void> {
        imageBody.tenantId = tenantAccessor.accessTenant().id
        val image = imageService.addImage(imageBody)
        return withMdc(image).executeAndReturn {
            imageMessagePublisher.publishCreatedEvent(image)
            val location = UriComponentsBuilder
                    .fromUriString(linkTo(methodOn(ImageController::class.java)
                            .createImage(imageBody)).toString() + SHOP_IMAGE_LOCATION_MAPPING)
                    .buildAndExpand(image.id)
                    .toUri()
            ResponseEntity.created(location).build()
        }
    }

    @DeleteMapping(/{imageId})
    fun deleteImage(@PathVariable imageId: UUID): ResponseEntity<Void> = imageRepository.findOne(imageId)?.let {
        withMdc(it).executeAndReturn {
            imageService.delete(it)
            ResponseEntity<Void>(NO_CONTENT)
        }
    } ?: ResponseEntity(NOT_FOUND)
}

Event Publisher

  • Bounded Context published Domain Events

  • Example: Messaging or Feeds

  • Other bounded context may subscribe to those events to react upon them.

Message
Upstream System
Down-stream System

Event Publisher

@Component
@Slf4j
@RequiredArgsConstructor
public class MailMessagePublisher {

    private final DomainMessagePublisher domainMessagePublisher;
    private final DomainMessageFactory domainMessageFactory;

    public void publishMailSentEvent(ClientResponse response) {
        publishMessage("mailgateway.mail.sent.event", response);
    }

    private void publishMessage(String messageType, ClientResponse response) {
        MailSendEventMessage mail = MailSendEventMessage.builder()
                .messageId(response.getId())
                .status(QUEUED)
                .build();
        DomainMessage<MailSendEventMessage> message = domainMessageFactory.create(messageType, mail);
        domainMessagePublisher.publish(message);
        log.info("Published message of type {}", message.getMessageType());
    }

    @AllArgsConstructor
    @Builder
    @Getter
    @JsonTypeName("mail-sent-event")
    static class MailSendEventMessage implements DomainMessagePayload {

        private String messageId;
        private MailStatus status;
    }
}

Event Publisher

@Slf4j
@MessagePublisher
@RequiredArgsConstructor
class ImageMessagePublisher(
        private val domainMessageFactory: EntityDomainMessageFactory,
        private val domainMessagePublisher: DomainMessagePublisher,
        private val storageApiUriBuilder: StorageApiUriBuilder
) {
    val log: Logger = LoggerFactory.getLogger(javaClass)

    fun publishCreatedEvent(image: Image) {
        publishMessageWithoutChangeSet(image, "shop.image.created.event")
    }

    fun publishDeletedEvent(image: Image) {
        publishMessageWithoutChangeSet(image, "shop.image.deleted.event")
    }

    private fun publishMessageWithoutChangeSet(image: Image, messageType: String) {

        val message = domainMessageFactory
                .prepareDomainMessageForType(messageType)
                .withEntity(image)
                .withPayloadType("image")
                .withCurrentTenant()
                .withoutChangeSet()
                .withAdditionalProperty("dataUri", toRelativeUri(image))
                .build()
        log.info("publishing message {}", message)
        domainMessagePublisher.publish(message)
    }
    private fun toRelativeUri(image: Image?): String? =
            image?.let { storageApiUriBuilder.getRelativeImageDownloadUri(it.dataUri).expand().toString() }
}

Context Map Patterns Categorized

Model Flow

 Upstream Patterns

  • Open Host Service

  • Event Publisher

 Downstream Patterns

  • Customer / Supplier

  • Conformist

  • Anticorruption Layer

 In-Between Patterns

  • Shared Kernel

  • Published Language

  • Seperate Ways

Upstream System
Down-stream System

Customer / Supplier

  • Two teams share a customer / supplier relation

  • The downstream team is considered to be the customer

  • Veto rights may be applicable

Veto
Upstream System
Down-stream System

Customer / Supplier

@Test
@PactVerification()
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {
  
    // when
    ResponseEntity<String> response = new RestTemplate()
      .getForEntity(mockProvider.getUrl() + "/pact", String.class);
 
    // then
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
    assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}

Consumer-Driven Contracts with Pact

Conformist

  • The downstream conforms to the model of the upstream team

  • No translation of models

  • No veto rights

Upstream System
Down-stream System
@RunWith(SpringRunner.class)
@ShopApplicationTest
public class SiteEventContractTest {

    @Autowired
    private MessageStubTrigger messageStubTrigger;
    @MockBean
    private SiteEventSubscriber siteEventSubscriber;
    @Captor
    private ArgumentCaptor<DomainMessage<SitePayload>> siteEventCaptor;
    @Captor
    private ArgumentCaptor<DomainMessage<SiteFeaturesPayload>> siteFeaturesEventCaptor;

    @Test
    public void should_process_site_created_event()  {
        messageStubTrigger.trigger("site.site.created.event");
        thenVerify(siteEventSubscriber).should().consumeSiteCreatedEvent(siteEventCaptor.capture());
        then(siteEventCaptor.getValue().getTenantId()).isNotNull();
        then(siteEventCaptor.getValue().getPayload().getLocale()).isNotNull();
    }

    @Test
    public void should_process_site_deleted_event()  {
        messageStubTrigger.trigger("site.site.deleted.event");
        thenVerify(siteEventSubscriber).should().consumeSiteDeletedEvent(siteEventCaptor.capture());
        then(siteEventCaptor.getValue().getTenantId()).isNotNull();
    }

    @Test
    public void should_process_site_features_updated_event()  {
        messageStubTrigger.trigger("site.site-features.updated.event");
        thenVerify(siteEventSubscriber).should().consumeSiteFeaturesUpdatedEvent(siteFeaturesEventCaptor.capture());
        then(siteFeaturesEventCaptor.getValue().getTenantId()).isNotNull();
        then(siteFeaturesEventCaptor.getValue().getPayload().getFeatures()).isNotEmpty();
    }
}

Conformist

Anticorruption Layer

  • A layer that isolates a client model from the provided model by translation

  • external vs. internal model

Upstream System
Down-stream System
public enum GoogleAvailability {

    @JsonProperty(value = "in stock")
    IN_STOCK("in stock"),
    @JsonProperty(value = "out of stock")
    OUT_OF_STOCK("out of stock"),
    @JsonProperty(value = "preorder")
    PRE_ORDER("preorder");

    private String name;

    GoogleAvailability(String name) {
        this.name = name;
    }

    public static GoogleAvailability fromAvailabilityState(AvailabilityState availability) {
        if (availability == null
                || availability.equals(AvailabilityState.IN_STOCK)
                || availability.equals(AvailabilityState.LOW_STOCK)) {
            return IN_STOCK;
        } else if (availability.equals(AvailabilityState.OUT_OF_STOCK)
                || availability.equals(AvailabilityState.NOT_AVAILABLE)) {
            return OUT_OF_STOCK;
        } else {
            return PRE_ORDER;
        }
    }
}

Anticorruption Layer

Context Map Patterns Categorized

Model Flow

 Upstream Patterns

  • Open Host Service

  • Event Publisher

 Downstream Patterns

  • Customer / Supplier

  • Conformist

  • Anticorruption Layer

 In-Between Patterns

  • Shared Kernel

  • Published Language

  • Seperate Ways

Upstream System
Down-stream System

Shared Kernel

  • Two team share a (subset of the) domain model

  • can be shared lib or a database

Upstream System
Down-stream System
public class DomainMessage<T extends DomainMessagePayload> {

    private String messageType;

    private LocalDateTime timestamp;

    private Integer tenantId;

    @JsonInclude(NON_EMPTY)
    private Collection<DiffItem> changeSet = emptyList();

    private T payload;

    private DomainMessage() {
        // needed by Jackson
    }

    @NotNull
    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    @Nullable
    public Integer getTenantId() {
        return tenantId;
    }

    @NotNull
    public String getMessageType() {
        return messageType;
    }

    @NotNull
    public Collection<DiffItem> getChangeSet() {
        return changeSet;
    }

    @NotNull
    public T getPayload() {
        return payload;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this) //
                .add("messageType", messageType) //
                .add("timestamp", timestamp) //
                .add("tenantId", tenantId) //
                .add("changeSet", changeSet) //
                .add("payload", payload) //
                .toString();
    }

    @NotNull
    public static <T extends DomainMessagePayload> DomainMessageBuilder<T> builder(@NotNull String messageType) {
        return new DomainMessageBuilder<>(messageType);
    }

  
    @JsonIgnore
    public boolean isChangeSetRelevantForPayload() {
        Set<String> possiblePathNames = Arrays.stream(BeanUtils.getPropertyDescriptors(payload.getClass()))
                .map(PropertyDescriptor::getName)
                .map(property -> "/" + property)
                .collect(toSet());
        return changeSet.stream()
                .map(DiffItem::getPath)
                .anyMatch(possiblePathNames::contains);
    }

    public static class DomainMessageBuilder<T extends DomainMessagePayload> {

        private final String messageType;

        private Integer tenantId;

        private LocalDateTime timestamp = LocalDateTime.now();

        private Collection<DiffItem> changeSet = emptyList();

        @NotNull
        private DomainMessageBuilder(@NotNull String messageType) {
            this.messageType = checkNotNull(messageType);
        }

        @NotNull
        public DomainMessageBuilder<T> timestamp(@NotNull LocalDateTime timestamp) {
            this.timestamp = checkNotNull(timestamp);
            return this;
        }

        @NotNull
        public DomainMessageBuilder<T> tenantId(@Nullable Integer tenantId) {
            this.tenantId = tenantId;
            return this;
        }

        @NotNull
        public DomainMessageBuilder<T> changeSet(@Nullable Collection<DiffItem> changeSet) {
            this.changeSet = changeSet;
            return this;
        }

        @NotNull
        public DomainMessage<T> build(@NotNull T payload) {
            requireJsonTypeInformation(checkNotNull(payload).getClass());
            final DomainMessage<T> domainMessage = new DomainMessage<>();
            domainMessage.messageType = this.messageType;
            domainMessage.timestamp = this.timestamp;
            domainMessage.tenantId = this.tenantId;
            domainMessage.payload = payload;
            domainMessage.changeSet = changeSet;
            return domainMessage;
        }
    }
}

Shared Kernel


dependencyManagement {
    imports {
        mavenBom "com.example.demo:shared-parent:1.2.3"
    }
}

dependencies {
    compile     ("com.example.demo:shared-message")
}

Shared Kernel

Published Language

  • Similar to Open Host Service

  • A common language between bounded context is modeled

Upstream System
Down-stream System

Published Language

@NoArgsConstructor(access = PRIVATE)
@AllArgsConstructor(access = PRIVATE)
@EqualsAndHashCode(callSuper = false)
@Getter
@Builder
@Embeddable
public class Identifier implements Serializable {

    private static final long serialVersionUID = -123242542L;

    @Enumerated(STRING)
    @NotNull
    @Column(name = "TYPE", length = 10, nullable = false)
    private IdentifierType type;

    @NotNull
    @Column(name = "VALUE", length = 255, nullable = false)
    private String value;
}


public enum IdentifierType {
    EAN,
    UPC,
    ISBN,
    MPN
}

Seperate Ways

  • No connection between bounded contexts exists

  • Teams can find their own solution for their domain

Upstream System
Down-stream System

Context Map Patterns Categorized

Model Flow

 Upstream Patterns

  • Open Host Service

  • Event Publisher

 Downstream Patterns

  • Customer / Supplier

  • Conformist

  • Anticorruption Layer

 In-Between Patterns

  • Shared Kernel

  • Published Language

  • Seperate Ways

Upstream System
Down-stream System

provider-ui

storefront-ui

merchant-ui

Frontend

api proxy

tenant

auth

business unit

site

shop

Shop Admin

Product

product

. . .

. . .

. . .

. . .

System Architecture

Core

DHGE Microservices

By Benjamin Nothdurft

DHGE Microservices

2020-09-21-microservices

  • 1,083