Benjamin Nothdurft
Software Craftsman, DevOps, @jenadevs @jugthde Founder/Speaker, Traveller & Cyclist – focusing on Continuous Delivery Pipelines, Automation, Docker, Selenium, Java
Top-Down View
Event Storming
System Architecture
Context Map Patterns
CQRS Deep-Dive Example
More CQRS Hands-On
Questions
Exe / Jar / Zip
Objects
Classes
OOP
Design Patterns
Modules
Layers
Project
Service
Exe / Jar / Zip
Objects
Classes
OOP
Design Patterns
Modules
Layers
Project
Service
Service
Service
Sub-Domain
Sub-Domain
Domain
Domain-Driven-Design
Exe / Jar / Zip
Objects
Classes
OOP
Design Patterns
Modules
Layers
Project
issues/hot spots, chances
external systems, read models, policies
users/actors, timers, loops
product added to cart
domain event (past tense)
orange sticky note
relevant for domain experts
command (present tense)
blue sticky note
triggers a domain event
product add to cart
aggregate/entity
yellow sticky note
data that is interacted with
product
find borders where a domain model has a different meaning
domain events flow between different models
bounded context/sub-domain
red sticky note
core name that is relevant/valid
checkout
narrative/stories/user journey
line(s)
multiple storytellings
common language for everyone
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
provider-ui
storefront-ui
merchant-ui
Frontend
api proxy
tenant
auth
Core
business unit
site
shop
Shop Admin
Product
product
. . .
. . .
. . .
. . .
Each sub-domain is implemented as vertical:
Ubiquitous Language
Bounded Context
Domain Model
A domain model is to a bounded context
what classes are to objects
Context Map
Call Flow
Model Flow
Call Flow
Model Flow
Downstream System
Upstream System
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
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
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
@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();
}
}
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)
}
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
@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;
}
}
@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() }
}
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
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
@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
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();
}
}
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;
}
}
}
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
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;
}
}
}
dependencyManagement {
imports {
mavenBom "com.example.demo:shared-parent:1.2.3"
}
}
dependencies {
compile ("com.example.demo:shared-message")
}
Similar to Open Host Service
A common language between bounded context is modeled
Upstream System
Down-stream System
@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
}
No connection between bounded contexts exists
Teams can find their own solution for their domain
Upstream System
Down-stream System
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
. . .
. . .
. . .
. . .
Core
By Benjamin Nothdurft
2020-09-21-microservices
Software Craftsman, DevOps, @jenadevs @jugthde Founder/Speaker, Traveller & Cyclist – focusing on Continuous Delivery Pipelines, Automation, Docker, Selenium, Java