ArchitectMind
  1. Home
  2. Tutorials
  3. Book 7 · Application Architecture & Distributed Systems
  4. Volume VI
  5. Event Types
Book 7 · Volume VI — Event-Driven & Asynchronous Architecture · Topic 1.2

Event Types

Domain, integration and notification events

8 min readStage 3 · Systems8 interview questions
Chapter
Chapter 1 — Event-Driven Architecture
Prerequisites
Topic 1.1; Volume V Chapter 2 (bounded contexts)
You will be able to
Distinguish domain, integration and notification events by audience, payload and contract strength, raise domain events from aggregates, translate them into integration events via an outbox, design thin notifications with call-back, and choose the right type per consumer relationship
Covers
Domain; Integration; Notification
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — Choosing per Relationship
  5. 4A. Deep Dive — Translation and Anti-Corruption
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

Not all events serve the same audience. Domain events express something meaningful that happened inside a bounded context, in its own language. Integration events are the curated, versioned facts a context publishes for other contexts. Notification events are thin signals — "something changed, here's its id" — that prompt consumers to fetch details if they need them.

Event types (TOC)
Figure 1 — Event types (TOC)
TOC typeDefinition
DomainAn event raised by the domain model (often by an aggregate) inside a bounded context, using its ubiquitous language and internal types, consumed by the same context's handlers; free to change with the model
IntegrationAn event published across bounded-context boundaries as part of the context's public contract — curated payload, stable schema, versioned, backward compatible, typically on a broker topic
NotificationA minimal event (type + identifier, perhaps a link) telling consumers that something changed without carrying the state; consumers call the source for current data if needed
Table 1 — The three TOC types

2Why It Exists

Publishing domain events directly couples every consumer to your internal model; publishing only notifications couples every consumer to your API at runtime. Separating types lets you keep internal freedom and external stability.

Each event type solves a different coupling problem, and using one type for everything recreates the problem the others avoid. Inside a context, fine-grained domain events let modules react to changes without direct calls, and they can change as freely as the model. Across contexts, consumers need stable facts with a schema, versioning and an owner, so the publishing team can still refactor internally. For external partners or sensitive data, a thin notification lets the source keep control of authorisation and avoids sending data to places it should not go. Recognising the audience of each event, private, internal-public or external, tells you how much contract, documentation and compatibility discipline it needs.

Scope of each event type
Figure 2 — Scope of each event type

3Core Concepts

3.1 Domain

JAVA
// Aggregate raises a domain event (Spring Data's AbstractAggregateRoot)
@Entity
class Order extends AbstractAggregateRoot<Order> {
    Order place() {
        this.status = Status.PLACED;
        // rich, internal
        registerEvent(new OrderPlacedDomainEvent(id, lines, pricing, placedAt));
        return this;
    }
}
 
// Same-context handler, after commit (Spring Modulith: transactional, async, retried)
@ApplicationModuleListener
void on(OrderPlacedDomainEvent e) { loyaltyModule.accrue(e.orderId(), e.pricing()); }
Domain event traitImplication
Internal types (value objects, enums)Not suitable for other teams
Fine-grainedMany per transaction
In-process or module-levelNo broker necessary
Changes with the modelNo compatibility promise
Table 2 — Domain events

3.2 Integration

Domain event → integration event
Figure 3 — Domain event → integration event
JAVA
// Translate at the boundary and externalise via the outbox (Spring Modulith)
@Component
class OrderEventTranslator {
    @ApplicationModuleListener
    void on(OrderPlacedDomainEvent e) {
        events.publishEvent(new OrderPlacedV1(               // published language
                UUID.randomUUID().toString(), e.orderId().value(), e.customerId().value(),
                e.totalPaise(), "INR", e.placedAt()));
    }
}
 
@Externalized("orders.events.v1::#{orderId()}")          // topic::key
record OrderPlacedV1(String eventId, String orderId, String customerId,
                     long totalPaise, String currency, Instant occurredAt) {}
Domain events vs integration events
Figure 4 — Domain events vs integration events

3.3 Notification

Notification event with callback
Figure 5 — Notification event with callback
JSON
{ "specversion": "1.0", "type": "shopnow.product.changed", "source": "/catalog-svc",
  "id": "c0a8…", "subject": "sku/SKU-4411", "time": "2026-09-26T10:02:00Z" }
Use notification whenAvoid when
Data changes often; consumer needs latestMany consumers (call-back storms)
Payload is large or sensitiveProducer availability is a concern
External partners via webhooksConsumer needs history of changes
Table 3 — When thin events fit

4How It Works Internally — Choosing per Relationship

Comparing event types
Figure 6 — Comparing event types
RelationshipRecommended type
Module A → module B in same serviceDomain event (in-process)
Context → other internal contextsIntegration event (ECST, Topic 1.3)
Context → external partnersNotification (webhook) + API
Context → analytics/data platformIntegration event or CDC
Table 4 — Type by relationship

4ADeep Dive — Translation and Anti-Corruption

Translation concernPractice
Hide internalsMap value objects to primitives/standard types
Stable namingBusiness vocabulary agreed with consumers
AggregationOne integration event may summarise several domain events
FilteringNot every domain event becomes public
VersioningType suffix .v1/.v2 + registry rules
Table 5 — Designing the published language
Typical payload size (illustrative)
Figure 7 — Typical payload size (illustrative)

5APIs / Features / Syntax

NeedTooling
Domain eventsAbstractAggregateRoot, ApplicationEventPublisher
After-commit handling@TransactionalEventListener, @ApplicationModuleListener
ExternalisationSpring Modulith @Externalized, outbox
NotificationsEventBridge, SNS, webhooks
DocumentationAsyncAPI, schema registry
Table 6 — Toolkit

6Practical Examples

6.1 Internal model leak

Order-svc serialised its JPA Order entity straight onto Kafka. Renaming an internal enum broke four consumer teams. Introducing OrderPlacedV1 integration events with an explicit mapper restored the team's freedom to refactor.

6.2 Partner webhooks

Marketplace sellers wanted order updates. ShopNow sends signed notification webhooks (order.updated + id); sellers call the partner API for details, which enforces authorisation per seller.

6.3 Worked example — classify

EventType
LineItemRepricedDomain
OrderPlacedV1Integration
product.changed webhookNotification
Table 7 — Audience-based

6.4 Worked example — filtering

Domain eventsPublished
DraftSaved, LineAdded, OrderPlacedOrderPlaced only
Table 8 — Not everything is public

6.5 Worked example — primitive types

InternalPublished
Money(1299.00, INR)totalPaise=129900, currency="INR"
Table 9 — Portable

6.6 Worked example — after commit

ListenerRuns
@TransactionalEventListener(AFTER_COMMIT)Only if tx committed
Table 10 — No phantom events

6.7 Worked example — Modulith registry

FeatureEffect
Event publication registryIncomplete publications retried on restart
Table 11 — Reliability in-process

6.8 Worked example — notification storm

Consumers × eventsCall-backs/min
12 × 3,50042,000
Table 12 — Switch to ECST

6.9 Worked example — webhook security

ControlDetail
HMAC signature headerVerify authenticity
Table 13 — External notifications

6.10 Worked example — naming convention

PatternExample
domain.entity.action.vNshopnow.order.placed.v1
Table 14 — Consistent types

6.11 Worked example — topic per context

TopicContains
orders.events.v1All order integration events
Table 15 — Ordering per order key

6.12 Worked example — aggregation

Domain eventsIntegration event
PaymentAuthorised + CapturedOrderPaid
Table 16 — Coarser public facts

6.13 Worked example — analytics consumers

NeedSource
Every field, historyCDC stream (not integration events)
Table 17 — Different contract

6.14 Worked example — AsyncAPI doc

YAML
channels:
  orders.events.v1:
    messages:
      OrderPlacedV1:
        payload: { $ref: '#/components/schemas/OrderPlacedV1' }

6.15 Worked example — in-process vs broker

ConsumerMechanism
Same service moduleDomain event
Other serviceIntegration event
Table 18 — Right channel

6.16 Worked example — PII

FieldNotification?
Customer addressOmit; fetch with auth
Table 19 — Thin for sensitive data

6.17 Worked example — versioning

Breaking changeNew type OrderPlacedV2
Table 20 — Chapter 6

6.18 Worked example — tests

TestChecks
Modulith Scenario testDomain event → integration event published
Table 21 — Verify translation

6.19 Worked example — ADR

DecisionRationale
Translate domain → integration at boundaryModel freedom
Table 22 — Recorded

6.20 Worked example — review checklist

Check✔
No entities on topics✔
Integration events versioned✔
Notifications for partners/PII✔
Catalogued in AsyncAPI✔
Table 23 — Event-type review

6.21 Worked example — interview answer

TypeAnswer
DomainInternal, model language
IntegrationPublic, versioned contract
NotificationThin, call back for data
Table 24 — Structured answer

7Common Problems & Mistakes

  • Publishing entities or internal domain events.
  • Notification storms with many consumers.
  • Events before commit.
  • No versioning of integration events.
  • Sensitive data in broadly consumed events.
  • Fine-grained public events mirroring internal steps.

Most of these mistakes come from not deciding who an event is for. Serialising JPA entities or internal domain events onto Kafka is the most common, and it makes every internal refactoring a cross-team breaking change. Notification-only designs hit the opposite problem when many consumers call back at once and overload the producer. Publishing before commit creates phantom events for transactions that later roll back; listeners must run after commit, or events must go through an outbox. Unversioned schemas break consumers silently. Personal data such as addresses ends up on broadly readable topics because adding a field was easy. Finally, public events that mirror every internal step, such as DraftSaved and LineAdded, expose the workflow and force consumers to reassemble business facts themselves.

8Performance & Production Considerations

AspectGuideline
Domain eventsIn-process, after commit
Integration eventsOutbox, registry, compatibility
NotificationsSigned, rate-aware callbacks
Payload sizeCurated, not dumps
DocsAsyncAPI catalog
Table 25 — Operational guidelines

9Architecture & Design Considerations

  • Keep domain events private.
  • Design integration events as published language.
  • Use notifications for partners, large or sensitive data.
  • Choose per consumer relationship.

10Interview Questions & Answers

Q1BasicConcept

What is a domain event?

A domain event is a record that something meaningful happened in the domain model of one bounded context, expressed in that context's ubiquitous language, such as OrderPlaced or LineItemRepriced. It is usually raised by an aggregate when a state change succeeds, for example through Spring Data's AbstractAggregateRoot.registerEvent, and published when the repository saves the aggregate. Its consumers are handlers in the same context or modular monolith, which react after commit using @TransactionalEventListener or Spring Modulith's @ApplicationModuleListener. Because it is private, it can use internal value objects and enums, be fine-grained and change whenever the model changes. That freedom is exactly why it should not be published directly to other teams.

Q2BasicConcept

What is a notification event?

A notification event is a thin signal that something changed, carrying only the event type, an identifier and perhaps a link, not the changed state. A CloudEvents message of type shopnow.product.changed with subject sku/SKU-4411 is typical. Consumers that care call back to the source's API to fetch the current data, so they always see the latest state and the producer's authorisation rules apply on every read. This is Fowler's event notification pattern. It suits external partners through signed webhooks, large or sensitive payloads such as customer addresses, and data that changes often. The cost is runtime coupling: consumers depend on the producer's API being available and fast.

Q3IntermediateTricky

Why not publish domain events directly to other services?

Because a domain event is part of your internal model, so publishing it turns your internal model into a public contract. Domain events use internal value objects, enums and names that change whenever the team refactors. Once other teams consume them, every rename or restructuring becomes a breaking change that needs coordination, so your context loses its freedom to evolve. Domain events are also fine-grained and may expose fields others should not see. The note's example: order-svc serialised its JPA entity to Kafka, and renaming one enum broke four consumer teams. The fix is to translate domain events at the boundary into curated, versioned integration events, such as OrderPlacedV1, using primitive and standard types.

Q4IntermediateConcept

What makes an integration event a contract?

An integration event is a contract because other contexts build on it, so the publisher promises stability. It is curated: only the fields consumers need, in agreed business vocabulary and portable types, for example totalPaise and currency instead of an internal Money object. It has an explicit schema in a schema registry such as Confluent or AWS Glue, with compatibility rules, usually backward compatibility, enforced on every change. It is versioned, with names like OrderPlacedV1 and topic orders.events.v1, and a breaking change creates a new version while the old one runs in parallel. It is documented in AsyncAPI with an owner, and reviewed like a public API before release.

Q5Senior EngineerTrade-off

When are notification events a poor choice?

Notification events are a poor choice when many consumers would call back for the same data, when the producer's availability is a concern, or when consumers need the history of changes. Each notification triggers a call per consumer, so twelve consumers and 3,500 events a minute produce 42,000 API calls, a call-back storm that can overload the producer at peak. Every consumer also depends at runtime on the producer's API, so its outage stops all of them. Because notifications are thin, a consumer that misses intermediate states cannot reconstruct what happened. In those cases event-carried state transfer, integration events with the needed state, is usually better, accepting larger payloads and eventual consistency.

Q6Senior EngineerInternals

How do you publish integration events reliably from domain events?

The key is to make publishing part of the same transaction as the state change, then deliver asynchronously. The aggregate raises a domain event, and a translator listens after commit and maps it to an integration event. With Spring Modulith, the event publication registry stores each publication in the database within the business transaction, and @Externalized routes the integration event to a Kafka topic with a key, so incomplete publications are retried after a restart. Without Modulith, the transactional outbox pattern does the same: write the event to an outbox table in the same transaction, then relay it with a poller or Debezium. Consumers must still be idempotent, because delivery is at least once.

Q7Technical ArchitectScenario

How do you serve analytics that need every field?

Give analytics a different contract instead of bloating integration events. Integration events are deliberately curated for operational consumers; adding every column to satisfy the data platform would expose internals, make the schema change constantly and couple every consumer to those changes. For analytics that need every field and full history, a change data capture stream from the database, using Debezium or AWS DMS into Kafka or a data lake, is usually better, ideally owned as a data product with documented semantics and access controls. Sensitive fields can be masked or tokenised in that pipeline. The operational integration events stay small and stable, and the analytics contract can evolve on its own terms.

Q8Solution ArchitectDesign

How would you govern event types across teams?

I would set a few clear rules and automate their checks. First, a naming convention such as shopnow.order.placed.v1 and one topic per context, keyed for ordering. Second, a rule that entities and domain events never go onto shared topics, enforced in review and with ArchUnit checks that published types live in an api package. Third, integration events are reviewed like public APIs, registered in a schema registry with backward-compatibility checks in CI, and documented in an AsyncAPI catalog with an owner. Fourth, notifications are the default for external partners and sensitive data, signed with HMAC. Finally, regular reviews look for notification storms and over-fine public events.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 7

Application Architecture & Distributed Systems

Volume I · Topic 1.1

Software Architecture

Architecture Fundamentals & Architectural Thinking

Volume I · Topic 1.2

Architecture Responsibilities

Architecture Fundamentals & Architectural Thinking

Volume I · Topic 1.3

Architect vs Developer

Architecture Fundamentals & Architectural Thinking

Volume II · Topic 1.1

Monolith Fundamentals

Application Architecture Styles & Patterns

Volume II · Topic 1.2

Types of Monoliths

Application Architecture Styles & Patterns

Volume II · Topic 1.3

Monolith Advantages

Application Architecture Styles & Patterns

Volume III · Topic 1.1

Domain-Driven Design

Domain-Driven Design & Business Boundaries

Volume III · Topic 1.2

Strategic vs Tactical DDD

Domain-Driven Design & Business Boundaries

Volume III · Topic 1.3

Domain Complexity

Domain-Driven Design & Business Boundaries

Volume IV · Topic 1.1

What Is a Distributed System?

Distributed Systems Fundamentals

Volume IV · Topic 1.2

Why Distributed Systems Exist

Distributed Systems Fundamentals

Volume IV · Topic 1.3

Distributed System Complexity

Distributed Systems Fundamentals

Volume V · Topic 1.1

What Makes a Microservice?

Microservices Architecture

Volume V · Topic 1.2

Service Granularity

Microservices Architecture

Volume V · Topic 1.3

Microservice Boundaries

Microservices Architecture

Volume VI · Topic 1.1

Event Fundamentals

Event-Driven & Asynchronous Architecture

Volume VI · Topic 1.3

Event-Carried State

Event-Driven & Asynchronous Architecture

Volume VII · Topic 1.1

Scalability

Resilience, Scalability & Distributed Data

Volume VII · Topic 1.2

Stateless Architecture

Resilience, Scalability & Distributed Data

Volume VII · Topic 1.3

Load Balancing

Resilience, Scalability & Distributed Data

Volume VIII · Topic 1.1

ADR

Architecture Decision-Making & System Evolution

Volume VIII · Topic 1.2

Architecture Decision Process

Architecture Decision-Making & System Evolution

Volume VIII · Topic 1.3

Reversible vs Irreversible Decisions

Architecture Decision-Making & System Evolution