Using domain events with legacy software

If you are using legacy software and you want to add new features without adding more complexity, this article describes Domain Driven Design, Clean Architecture, or CQRS techniques based on a real life example

by Pascal Garcia and Baptiste Mesta, R&D Engineers at Bonitasoft

If you are using legacy software that is mature and working well, but has a large, complex code base — and you want to add new features without adding more complexity, we have a way of doing that with Domain Driven Design, Clean Architecture, or CQRS techniques that could help you.

This article offers a step by step guide on how to add this feature on your own legacy software based on a real life example.

Context and methodology

This software handles its business logic mostly through Create Read Update Delete (CRUD) operations. The goal is to produce domain events from these operations, that is, what happened in the system at a business level.

We could refactor the software but that comes at a cost. Instead, to quickly start playing with domain events, we chose to develop an extension in the existing software to produce them.

The software itself is a classic Java application, based on Spring and Hibernate, and all code samples in this article are in Kotlin.

Here are the steps we followed.

  1. Design domain events
  2. Assess what is possible with current technical events
  3. Create domain events
  4. Publish the domain events

Design domain events

But, be careful, these domain events should represent what is happening in the business and not address only a single current business need.

In our case, we want to do analytics on business processes. This means that we want to have an event each time a change happens on the business process, for example:

  • when a new case of a process starts
  • when it ends
  • when a task of that process is submitted

The best way we found to design these events is to design them as you would for a greenfield development.

The goal is to create domain events that are as close as possible to the business logic.

For example, for the domain event for the “new case of a process starts,” we named the event “CASE_STARTED” to contain information about the user that started that case, the data the user started the case with, and what the process related to that case is.

We strongly advise you to read about Domain Driven Design techniques. There are many articles on how to design these events and how to understand the business, and we have listed some references at the end of this article.

In addition, this phase should be done with stakeholders who know and understand the business processes.

Assess what is possible with current technical events

As a second step, dive into your current codebase and list all data you will be able to extract from current technical events, already produced. This could be data from:

  • listeners already implemented in your software
  • listeners on your framework (as most frameworks provide ways to listen to changes, explore them and verify what information you can gather from them)
  • probes on your system
  • combination of a set of all other data. As a trivial example: if you have the start state of something, combined with the current date, you have the duration of that event.

The illustration below shows the simplified architecture of our software.

In our case, we can use several extension points:

  • Transaction synchronization
  • Event handlers
  • Hibernate event listeners

In the next section, we’ll explain more how these can be leveraged for our needs.

Adapt the design in collaboration with the stakeholders

  • Can you find a smaller functional scope for your events in that first iteration?
  • Is it possible to keep your original design by using post processing?
  • Can you identify what needs to change in the software to have the full scope of functionality?

You might now have a first version for your domain event, along with a backlog of improvements for the software and your events.

For example, in our case, we were not able to link the update on what we call BusinessData to the Start of a case. What we decided was to simply record both events separately, and in a future iteration add post-processing to combine them.

Check that the events follow some important rules

  • Immutable
  • Unique: you’ll need to generate a unique identifier.
  • Set in the past: all events should be named in the past tense
  • Have a timestamp
  • Have a context which will help identify the scope of that event

The book Implementing Domain-Driven Design[1] details in great depth what rules events should follow.

How to create domain events

CASE_STARTED, CASE_COMPLETED, TASK_STARTED, TASK_ASSIGNED, TASK_UNASSIGNED, TASK_SUSPENDED, TASK_EXECUTED, TASK_COMPLETED, CONNECTOR_STARTED, CONNECTOR_COMPLETED, BDM_INSERTED, BDM_UPDATED

and the following event structure:

{
“id”: “6c3b3501-f454–4eb3-be55–63176f47e767”,
“tenantId”: 1,
“name”: “CASE_STARTED”,
“bpmObjectType”: “CASE”,
“bpmObjectName”: “LoanRequest”,
“caseExecutorId”: 1,
“contractData”: {
“amount”: 100000,
“type”: “house”
},
“businessData”: {
“loanData”: {
“ids”: [
84
],
“dataClassName”: “com.company.loan.Loan”
}
},
“timestamp”: 1582712293203,
“context”: {
“processId”: 7564421046497327000,
“caseId”: 1052,
“rootCaseId”: 1052,
“executorId”: 1
}
}

Once we have our domain events correctly defined, we can start creating them. We will describe more technical aspects of what we did there.

Datasource at our disposal

The other data source was the Hibernate entity manager on which you can register: org.hibernate.event.spi.PostUpdateEventListener and org.hibernate.event.spi.PostInsertEventListener

These listeners are registered on the Hibernate session factory, and are called when an entity is updated or inserted.

Below is an example of registration for these handlers:

val registry =
sessionFactory.serviceRegistry.getService(EventListenerRegistry::class.java) registry.appendListeners(EventType.POST_INSERT, bdmHibernateInsertListener) registry.appendListeners(EventType.POST_UPDATE, bdmHibernateUpdateListener)

For more information on Hibernate listeners, see https://vladmihalcea.com/hibernate-event-listeners/

Process technical events

The following schema summarizes that flow:

We are working in a transactional environment. This means that all actions will really be applied only at the end of the transaction. The choice we made was to register a javax.transaction.Synchronization and to gather all technical events during the execution of the transaction, and publish them at the commit of the transaction.

Here is how we register a transaction synchronization on the transaction manager:

private val bonitaEventSynchronizations:
ThreadLocal <bonitaeventsynchronization>= ThreadLocal()</bonitaeventsynchronization>
private fun getSynchro(): BonitaEventSynchronization {
return bonitaEventSynchronizations.getOrSet {
val bonitaEventSynchronization = BonitaEventSynchronization(bonitaEventSynchronizations, bonitaEventProcessor, domainEventPublisher)
txManager.getTransaction().registerSynchronization(bonitaEventSynchronization);
}
}

When the transaction is completed, we combine these technical events into one or more domain events.

The goal here is to gather enough information, from technical events (BonitaEventTypes), to avoid having to actively retrieve missing information. This will ensure that producing these new events will have a minimal impact on performance.

We have a set of DomainEventProcessor, each one producing a single type of domain event.

interface DomainEventProcessor {
fun createDomainEvent(events: MutableList<sevent>): List <domainevent>
}

For example, the domain event processor below is in charge of publishing the TASK_STARTED domain event:

class TaskStartedProcessor : DomainEventProcessor {
companion object {
val eventTypes = setOf( ACTIVITYINSTANCE_CREATED.name, EVENT_INSTANCE_CREATED.name, GATEWAYINSTANCE_CREATED.name)
}
override fun createDomainEvent(events: MutableList<sevent>): List <domainevent>{
return events.stream().filter {
eventTypes.contains(it.type) }.map {
val activity = it.objectas SFlowNodeInstance
val taskStartedEvent = TaskDomainEvent(UUID.randomUUID(), activity.tenantId, TASK_STARTED, activity.toBPMObjectType(), activity.name) taskStartedEvent.timestamp = activity.lastUpdateDate taskStartedEvent.context = activity.toContext() return@map taskStartedEvent;
}.toList()
}
}

How to publish domain events

Once all domain events are created, we send it to a domain event publisher (DomainEventPublisher). The publisher should use its own thread to ensure minimal impact on the rest of the software execution.

private val executor: ExecutorService = Executors.newSingleThreadExecutor(CustomizableThreadFactory("pulsar-events"))

Each time the transaction completes, these events are pushed as below.

executor.submit {
producer?.send(domainEvent)
}

In our case, we publish these events to the broker Apache Pulsar. When the transaction is committed, the Pulsar client handles the serialization of domain events to a JSON format and sends them to the Pulsar broker.

Finally, by configuring a connector in Pulsar, we can publish wherever we want. In our case we publish to an Elastic search server (see https://pulsar.apache.org/docs/en/io-elasticsearch/).

Conclusion

The approach we took gave us a technical solution to our business need — which was to have domain events published to a data store that is easy to query in order to do data analytics on it.

As this approach is easy and quick to implement, it’s also a great opportunity to try out domain events and validate if they are well designed before implementing them directly in the core of the software.

At a low entry cost, it opens new possibilities of creating values with less coupling from the legacy software. All we have to do now is subscribe to these events.

In our case we were able to experiment with doing process analytics from these events, satisfying the original business requirement.

References

[1] Implementing Domain-Driven Design: ISBN-13 : 978–0321834577
[2] Domain-Driven Design: Tackling Complexity in the Heart of Software: ISBN-13 : 978–0321125217
[3] https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation
[4] https://www.martinfowler.com/eaaDev/DomainEvent.html
[5] https://vladmihalcea.com/hibernate-event-listeners/

This article was originally published in dev.to.

Bonitasoft helps innovative companies worldwide deliver better digital user experiences — for customers and employees — on the Bonita application platform.