Communication and Data Contracts - Building Event-Driven Microservices
Communication and Data Contracts - Building Event-Driven Microservices
PREV NEXT
⏮ ⏭
2. Event Driven Microservice Fundamentals 4. Integrating Event-Driven Architectures with Existing Systems
🔎
Introduction
The simplest pattern in communication theory is that of a sender seeking to
provide a message to a receiver via a specific channel. The sender and receiver
must have a common understanding of the message, otherwise the message may
be misunderstood and the communication will be incomplete. In the event-driven
ecosystem, the event is the message, and it is the fundamental unit of
communication. An event must describe as accurately as possible what happened,
and why. It is a statement of fact, and combined with all the other events in a
system provides a complete history of what has happened.
There are two components of a well-defined data contract. The first component is
the data definition, or what will be produced. It is a description of the data that
will be created, namely the fields, types and various data structures. The second
component is the triggering logic, or why it is produced. This is a description of
the fulfillment of specific business logic which triggers the creation of the event.
Changes can be made to both the data definition and the triggering logic,
depending on business requirement changes.
Care must be taken when changing the data definition, so as not to delete or alter
fields that are being used by downstream consumers. Similarly, care must also be
taken when modifying the triggering logic definition. It is far more common to
change the data definition than the triggering mechanism, as altering the
definition of when an event is triggered often breaks the meaning of the original
event definition.
The best way to enforce data contracts and provide consistency is to define a
schema for each event. The producer defines an explicit schema detailing the
data definition and the triggering logic, with all events of the same type adhering
to this format. In doing so, the producer provides a mechanism for
communicating its event format to all prospective consumers. The consumers, in
turn, can confidently build their microservice business logic against the
schematized data.
With an implicit schema, consumers must often rely on tribal knowledge and
inter-team communication to resolve data issues. This is not scalable as the
number of event streams and teams increase. There is also substantial risk in
requiring each consumer to independently interpret the data. A consumer may
interpret it in a way that is different than its peers, which leads to inconsistent
views of the single source of truth.
It may be tempting to build a common library that interprets any given event for
all services, but this runs into problems with multiple language formats and
constrains independent release cycles. The duplication of efforts across services
to ensure a consistent view of implicitly defined data is non-trivial, and is best
avoided completely.
Producers are also at a disadvantage with implicit schemas. Even with the best of
intentions, a producer may not notice (or perhaps their unit tests don’t reveal) that
they have altered their event data definition. Without an explicit check of their
service’s event format, it may go unnoticed until it causes downstream consumers
to fail. Explicit schemas give security and protection to both consumers and
producers.
Support for integrated comments and arbitrary metadata in the schema definition
is essential for communicating the meaning of an event. The knowledge
surrounding the production and consumption of events should be kept as close as
possible to the event definition. Schema comments support serves two very
important issues surrounding the use of schemas.
The first major benefit is being able to define the triggering logic of when an
event using that schema is produced, typically done in a block header at the top
of the schema definition. The second major benefit is the annotation of the fields
within the schema, where additional context and clarity can be provided by the
producer of the data about the particular field. These schema definition
comments help remove ambiguity as to meaning of data and reduces the chance
of misinterpretation by consumers.
The schema format must support a full range of schema evolution rules. Schema
evolution provides a means for producers to update their service’s output format
while allowing consumers to continue consuming the events uninterrupted.
Business changes may require new fields be added, old fields be deprecated, or
You have 2 days le in
the your
scope of atrial, Reddychan.
field be expanded. Subscribe
A schema evolution today.
framework ensures that See pricing options.
https://learning.oreilly.com/library/view/building-event-driven-microservices/9781492057888/ch03.html 1/5
4/9/2020 3. Communication and Data Contracts - Building Event-Driven Microservices
these changes can occur safely, and that producers and consumers can be updated
independently of one another.
An explicit set of schema evolution rules goes a long way in providing both
consumers and producers the ability to update their applications in their own
time. These rules are known as compatibility types.
Code Generator Support allows for an event schema to be turned into a class
definition or equivalent structure for a given programming language. This class
definition is used by the producer to create and populate new event objects. The
producer is required by the compiler or serializer (depending on the
implementation) to respect data types and populate all non-nullable fields that are
specified in the original schema. The objects created by the producer are then
converted into their serialized format and sent to the event broker, as shown in
Figure 1.
You may feel inclined to choose a more flexible option in the form of plain text
events using simple key-value pairs, which still offers some structure but
provides no explicit schema or schema evolution frameworks. Caution is advised
with this approach as the ability of microservices to remain isolated from one
another via a strong data contract become compromised, requiring far more inter-
team communication.
TIP
You have 2 days le in your trial, Reddychan. Subscribe today. See pricing options.
https://learning.oreilly.com/library/view/building-event-driven-microservices/9781492057888/ch03.html 2/5
4/9/2020 3. Communication and Data Contracts - Building Event-Driven Microservices
Designing Events
There are a number of best practices to follow when creating event definitions, as
well as several anti-patterns to avoid. Keep in mind that as the architectures
powered by event-driven microservices expand, the number of event definitions
expands. Well-designed events will minimize the otherwise repetitive pain points
for both consumers and producers. With that being said, none of these are hard-
and-fast rules. You can break them as you see fit, though I recommend that you
think very carefully about the full scope of implications and the trade-offs for
your problem space before proceeding.
The truth, the whole truth, and nothing but the truth
A good event definition is not a message just indicating that something happened,
but is the complete description of everything that happened during that event. In
business terms, this is the resultant data that is produced when input data is
ingested and the business logic is applied. This output event must be treated as
the single source of truth and must be recorded as an immutable fact for
consumption by downstream consumers. It is the full and total authority on what
actually occurred, and no other source of data should need to be consulted to
know that such an order took place.
Use the narrowest types for your event data. This lets you rely on the code
generators, language type checking (if supported) and serialization unit tests to
check the boundaries of your data. It sounds simple, but there are many cases
where ambiguity can creep in without using the proper types. Here are a few
easily avoidable examples of what I have seen.
Using string to store a numeric value. This requires the consumer to parse
and convert the string to a numeric value, and often comes up with GPS
coordinates. This is error prone and subject to failures, especially when a
value of “null” or an empty string are sent.
Using integer as a boolean. While 0 and 1 can be used to denote false and
true respectively, what does 2 mean? How about -1?
Enums are often avoided because producers fear creating a new enum token that
isn’t present in the consumer’s schema. However, the consumer has the
responsibility to consider enum tokens that it does not recognize, and determine
if it should process them using a default value, or simply throw a fatal exception
and halt processing until a human can work out what needs to be done. Both
Protobuf and Avro have elegant ways of handling unknown enum tokens, and
these should be used whenever possible.
There are several main problems with this approach. Each of these type
parameter values usually has a fundamentally different business meaning, even if
their technical representation is nearly identical. It is also possible for these
meanings to change over time and the scope that an event covers can creep.
Some of these types may require the addition of new parameters to track specific
type-centric information, whereas other types require other separate parameters.
Eventually it is possible to reach a situation where there are several very distinct
events all inhabiting the same event schema, making it difficult to reason about
what the event truly represents.
This complexity not only affects the developers who must maintain and populate
these events, but it also proves difficult for users to consume this data.
Consumers of this data need to have a consistent understanding about what data
is published and why it is published. If the data contract changes, it is expected
that they be able to isolate themselves from those changes. Adding additional
field types requires that they must therefor filter-in only data that they care
directly about. There is a risk that the consumer of the data fails to fully
understand the various meanings behind the types, leading to incorrect
consumption and logical wrong code. Additional processing must also be done to
discard messages that the consumer does not care about, with the implementation
replicated between each consumer.
It is very important to note that the underlying complexity inherent in the data
being produced is not reduced or solved by adding type fields. In fact, this
complexity is merely shifted from multiple distinct event streams with distinct
schemas, to a union of all the schemas merged into one event stream. In fact, the
complexity can even be said to have increased significantly due to this
entanglement. Future evolution of the schema is made more difficult as is
maintaining the code that produces the events.
Remember the principles of the data contract definition. Events should be related
to a single business action, not a generic type of event that records large
assortments of different kinds of data. If it seems like you need to have a generic
type of event with various different type parameters, consider if your business
problem is well-defined. This is usually a tell-tale sign that your problem space
and bounded context is not well defined.
E X A M P L E - OV E R LOA D I N G E V E N T D E F I N I T I O N S
There exists a simple website where a user can read a book or watch a movie.
When the user proceeds to engage with the website, say by opening the book up
or by starting the movie, a backend service publishes an event of this
engagement, named ProductEngagement, into an event stream. The data structure
of this cautionary-tale event may look something like this.
ProductEngagement {
productId: Long,
You have 2 days le in your trial, Reddychan. Subscribe today. See pricing options.
productType: TypeEnum,
https://learning.oreilly.com/library/view/building-event-driven-microservices/9781492057888/ch03.html 3/5
4/9/2020 3. Communication and Data Contracts - Building Event-Driven Microservices
actionType: ActionEnum
}
A new business requirement comes in - we want to track who watched the movie
trailer before watching the movie. There are no previews for the books we serve,
and though a boolean would suit the movie watching case, we need to allow it to
be nullable for book engagements.
ProductEngagement {
productId: Long,
productType: TypeEnum,
actionType: ActionEnum,
//Only applies to type=Movie
watchedPreview: {null, Boolean}
}
At this point, it can be seen that watchedPreview has nothing to do with Books,
but it’s added into the event definition anyways since we’re already capturing
product engagements this way. If we are feeling particularly helpful to our
downstream consumers, we can add a comment in the schema to tell them that
this field is only related to type=Movie.
One more new business requirement comes in - we want to track users that place
a bookmark in their book, and what page it is on. Again, because we have a
single defined structure of events for product engagements, our course of action
is constrained to adding a new action entity (Bookmark) and adding a nullable
PageId field.
ProductEngagement {
productId: Long,
productType: TypeEnum,
actionType: ActionEnum,
//Only applies to type=Movie
watchedPreview: {null, Boolean},
//Only applies to type=Book,Action=Bookmark
pageNumber: {null, Int}
}
As you can see by now, just a few changes in business requirements can greatly
complicate a schema that is trying to serve multiple business purposes. This puts
added complexity on both the producer and consumer of the data, as they both
must check for the validity of the data logic. The complexity of the data to be
collected and represented will always exist, but by following single responsibility
principles you can decompose the schema into something more manageable.
Let’s see what this would look like if we split each schema up according to single
responsibilities:
MovieClick {
movieId: Long,
watchedPreview: Boolean
}
BookClick {
bookId: Long
}
BookBookmark {
bookId: Long,
pageNumber: Int
}
The productType and actionType enumerations are now gone, and the schemas
have been flattened out accordingly. There are now three event definitions
instead of just a single one, and while the schema definition count has increased,
the internal complexity of each schema is greatly reduced. Following the
recommendation of one event definition per stream would see the creation of a
new stream for each event type. Event definitions would not drift over time, the
triggering logic would not change, and consumers could be secure in the stability
of the singular purpose event definition.
The takeaway from this example is not that the original creator of the event
definition made a mistake. In fact, at the time, the business only cared about any
product engagements but no specific product engagement, and so the original
definition is quite reasonable. As soon as the business requirements changed to
be able to track movie-specific behaviour, the owner of service needed to re-
evaluate if the event definition was still serving a single purpose, or if it was
instead now covering multiple purposes. Due to the business change of needing
finer resolution to lower-level details of an event, it became clear that, while the
event could serve multiple purposes, it soon would become complex and
unwieldy to do so.
It’s worth spending time to determine how your schemas may evolve over time.
Identify the main business purpose of the data being produced, the scope, the
domain, and if you are building it in such a way that has a singular purpose.
Validate that the schemas accurately reflect business concerns, especially for
systems that cover a broad scope of business function responsibility. It could be
that the there is misalignment between the business scope and the technical
implementation. Finally, evolving business requirements may require that the
event definitions be revisited and changed beyond just incremental definitions of
a single schema. Events may need to be split up and redefined completely should
sufficient business changes occur.
Events work well when they’re small, well-defined and easily processed. Large
events can and do happen though. Generally these larger events are representing
a lot of contextual information. Perhaps they have collected many data points that
are related to the given event, and are simply just a very large measurement of
something which occurred.
There are several considerations when looking at a design that produces a very
large event. Ensure that the data is directly related to the event and relevant to the
event that happened. Additional data may have been added to an event “just in
case”, but may not be of any real use to the downstream consumers. Ensure that
the data within is directly related and relevant to the business needs at hand. If
you find that all the event data is indeed directly related, take a step back and
look at your problem space. Does your microservice require access to the data?
You may wish to evaluate the bounded context to see if the service is performing
a reasonably sized amount of work. Perhaps the service could be reduced in
scope with additional functionality split off into its own service.
This scenario is not always avoidable though - some event processors produce
very large output files (perhaps a large image) that are much too big to fit into a
single message of an event stream. In these scenarios a pointer to the actual data
may be used, but this approach should be used sparingly. This adds risk in the
form of multiple sources of truth and payload mutability, as an immutable ledger
cannot ensure the preservation of data outside of its system. This is a design
tradeoff though, and it can be used when the need arises.
https://learning.oreilly.com/library/view/building-event-driven-microservices/9781492057888/ch03.html 4/5
4/9/2020 3. Communication and Data Contracts - Building Event-Driven Microservices
Consider a very simple example where a system outputs an event indicating that
“work is completed” for an arbitrary job. Although the event itself indicates the
work is done, the actual result of the work is not included in the event. This
means that to consume this event properly, you must find where the completed
work actually resides. Once there are two sources of truth for a piece of data,
consistency problems become very real.
Summary
In this chapter we looked at the need for a data contract between event-driven
microservices, and the tradeoffs between explicit and implicit schemas. I
provided a number of guidelines to show the need for focused, singular-purpose
events, and illustrated some of the pitfalls that tend to happen over time with
poorly-designed event schemas.
av
You have 2 days le in your trial, Reddychan. Subscribe today. See pricing options.
https://learning.oreilly.com/library/view/building-event-driven-microservices/9781492057888/ch03.html 5/5