Build CoreOrdered learning track

Notification and Communication Service

Learn Enterprise CPQ OMS Camunda 7 - Part 034

Mendesain notification dan communication service untuk CPQ/OMS enterprise: event-driven notification, template governance, idempotency, retries, delivery lifecycle, preferences, audit, channel adapter, dan workflow integration.

16 min read3037 words
PrevNext
Lesson 3464 lesson track1335 Build Core
#java#microservices#cpq#oms+6 more

Part 034 — Notification and Communication Service

Notification service sering dianggap gampang:

event masuk -> kirim email

Untuk CPQ/OMS enterprise, model ini terlalu rapuh.

Komunikasi quote/order membawa dampak besar:

  • customer menerima proposal;
  • approver diberi tugas;
  • sales diberi tahu quote expired;
  • fulfillment team diberi tahu order stuck;
  • support diberi notifikasi fallout;
  • customer dikirim order confirmation;
  • legal/commercial artifact dikirim keluar;
  • SLA escalation dipicu;
  • external partner diberi callback.

Jika notification salah, dampaknya bukan sekadar email gagal. Dampaknya bisa:

  • customer menerima quote salah;
  • customer menerima dokumen superseded;
  • approver melewatkan deadline;
  • order terlambat karena fallout tidak terlihat;
  • duplicate email membuat customer bingung;
  • sensitive data bocor;
  • audit tidak bisa membuktikan komunikasi;
  • workflow lanjut padahal delivery gagal;
  • service retry mengirim pesan berkali-kali.

Prinsip utama part ini:

Notification is not a side effect. It is a communication lifecycle with business evidence.

Notification service bukan sekadar SMTP wrapper. Ia adalah communication control plane.


1. Notification vs Communication

Kita mulai dari istilah.

TermMeaningExample
NotificationA message intended to inform or prompt actionapproval reminder email
CommunicationBroader business interaction with recipientquote proposal delivery
Delivery AttemptOne try through one channel/provideremail sent via provider X
Delivery ResultProvider/system outcomeaccepted, bounced, failed
RecipientTarget person/systemcustomer contact, approver, webhook consumer
TemplateVersioned content patternquote-approved-email:2026.07.0
MessageConcrete rendered contentactual email body sent
Communication RecordAuditable record of intent, content metadata, recipient, statusstored evidence

Tidak semua notification punya legal significance. Tetapi semua notification yang memengaruhi quote/order lifecycle harus punya audit trace.


2. Communication Types in CPQ/OMS

TypeExampleBusiness criticalityNeeds artifact?
Approval Task Notificationapprover assignedhighno
Approval Reminderpending too longmediumno
Quote Proposal Deliverysend quote PDF to customervery highyes
Quote Expiry Warningquote expires in 3 daysmediumsometimes
Quote Accepted Confirmationcustomer accepted quotehighyes/evidence
Order Confirmationorder createdhighyes/sometimes
Fulfillment Updateshipment/provisioning updatemediumno/sometimes
Fallout Alertmanual intervention neededhighno
SLA Escalationorder stuck beyond SLAhighno
External Webhooknotify partner/CRM/Billinghighpayload
Internal Digestdaily sales summarylow/mediumno

Criticality menentukan:

  • retry policy;
  • audit depth;
  • content retention;
  • approval requirement;
  • recipient validation;
  • delivery confirmation requirement;
  • fallback channel;
  • escalation behavior.

3. Architecture Overview

Notification service menerima business facts dan commands, lalu membuat communication records.

Ia tidak boleh menjadi tempat business state utama quote/order.

Ia boleh menyimpan communication evidence.


4. Core Responsibilities

Notification/Communication Service bertanggung jawab atas:

  • memilih notification rule berdasarkan event/command;
  • resolve recipient;
  • check preference/consent policy;
  • resolve template version;
  • render message;
  • attach/link artifact safely;
  • enqueue delivery attempt;
  • integrate with provider/channel;
  • retry delivery;
  • record delivery result;
  • publish communication events;
  • expose operational communication history;
  • provide audit evidence.

Ia tidak bertanggung jawab atas:

  • menghitung harga;
  • menentukan approval policy;
  • membuat quote document artifact;
  • mengubah order fulfillment state tanpa command contract;
  • menjadi CRM master data;
  • menjadi template storage tanpa governance;
  • mengirim dari workflow tanpa record.

5. Notification Lifecycle

Tidak semua channel mendukung semua status. Email provider mungkin memberi accepted/bounced/opened. Webhook punya HTTP status. In-app punya read/unread.

Lifecycle harus channel-neutral tetapi cukup spesifik untuk operasional.


6. Communication Record Model

Communication record adalah source of truth untuk komunikasi.

{
  "communicationId": "com-901",
  "tenantId": "tnt-123",
  "communicationType": "QUOTE_PROPOSAL_DELIVERY",
  "status": "SENT",
  "subjectType": "QUOTE_REVISION",
  "subjectId": "qr-004",
  "quoteId": "q-10029",
  "orderId": null,
  "artifactIds": ["art-77a"],
  "recipient": {
    "type": "CUSTOMER_CONTACT",
    "contactId": "contact-551",
    "channel": "EMAIL",
    "addressMasked": "a***@acme.example"
  },
  "templateVersion": "quote-proposal-email:2026.07.0",
  "idempotencyKey": "quote-q-10029-r4-proposal-delivery-contact-551",
  "requestedBy": "user-991",
  "requestedAt": "2026-07-02T10:20:00Z",
  "sentAt": "2026-07-02T10:20:03Z",
  "correlationId": "corr-abc"
}

Jangan hanya mengandalkan provider message id. Provider id adalah external attempt identity, bukan business communication identity.


7. Database Model

Communication table:

CREATE TABLE communication_record (
  communication_id        uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  communication_type      text NOT NULL,
  status                  text NOT NULL,
  subject_type            text NOT NULL,
  subject_id              uuid NOT NULL,
  quote_id                uuid,
  order_id                uuid,
  recipient_type          text NOT NULL,
  recipient_ref           text NOT NULL,
  channel                 text NOT NULL,
  address_hash            text,
  address_masked          text,
  template_family         text NOT NULL,
  template_version        text NOT NULL,
  rendered_content_hash   text,
  idempotency_key          text NOT NULL,
  requested_by            text,
  requested_at            timestamptz NOT NULL,
  sent_at                 timestamptz,
  final_at                timestamptz,
  failure_code            text,
  failure_message         text,
  correlation_id          text,
  created_at              timestamptz NOT NULL DEFAULT now(),
  updated_at              timestamptz NOT NULL DEFAULT now(),
  version                 bigint NOT NULL DEFAULT 0,

  CONSTRAINT uq_communication_idempotency UNIQUE (tenant_id, idempotency_key)
);

Attempt table:

CREATE TABLE communication_delivery_attempt (
  attempt_id              uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  communication_id        uuid NOT NULL REFERENCES communication_record(communication_id),
  attempt_no              int NOT NULL,
  channel                 text NOT NULL,
  provider                text,
  provider_message_id     text,
  status                  text NOT NULL,
  request_payload_hash    text,
  response_code           text,
  response_summary        text,
  attempted_at            timestamptz NOT NULL,
  completed_at            timestamptz,

  CONSTRAINT uq_communication_attempt_no UNIQUE (communication_id, attempt_no)
);

Artifact link:

CREATE TABLE communication_artifact_link (
  communication_id        uuid NOT NULL REFERENCES communication_record(communication_id),
  artifact_id             uuid NOT NULL,
  artifact_role           text NOT NULL,
  PRIMARY KEY (communication_id, artifact_id)
);

Outbox:

CREATE TABLE communication_outbox (
  outbox_id               uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  aggregate_type          text NOT NULL,
  aggregate_id            uuid NOT NULL,
  event_type              text NOT NULL,
  event_version           int NOT NULL,
  payload                 jsonb NOT NULL,
  headers                 jsonb NOT NULL,
  status                  text NOT NULL DEFAULT 'PENDING',
  created_at              timestamptz NOT NULL DEFAULT now(),
  published_at            timestamptz
);

Content retention perlu dipikirkan. Menyimpan full rendered body mungkin membantu audit, tetapi meningkatkan risiko privacy/sensitive data. Alternatifnya simpan hash, template version, render data reference, dan delivery metadata.


8. Command vs Event-Triggered Notification

Ada dua mode utama.

8.1 Command-triggered

User/system explicitly requests communication.

Example:

POST /quotes/{quoteId}/revisions/{revisionNo}/communications/send-proposal
Idempotency-Key: send-proposal-q-10029-r4-contact-551

Command-triggered cocok untuk:

  • send proposal;
  • resend proposal;
  • send order confirmation;
  • manual fallout escalation;
  • customer-specific communication.

8.2 Event-triggered

Business event triggers rule.

Example:

QuoteSubmittedForApproval -> notify approver
OrderFulfillmentFailed -> notify operations group
DocumentArtifactAvailable -> if delivery intent exists, send proposal

Event-triggered cocok untuk:

  • task assignment;
  • reminders;
  • SLA warnings;
  • operational alerts;
  • system callbacks.

Aturan penting:

Critical customer communication should often be command-triggered or workflow-controlled, not blindly triggered by low-level events.

DocumentArtifactAvailable tidak selalu berarti “kirim ke customer”. Bisa jadi artifact dibuat untuk preview, internal approval, atau manual review.


9. Notification Rule Model

Rule menentukan kapan komunikasi dibuat.

Example:

{
  "ruleId": "rule-approval-assigned-v1",
  "triggerEventType": "ApprovalTaskAssigned",
  "communicationType": "APPROVAL_TASK_ASSIGNED",
  "recipientResolver": "APPROVAL_ASSIGNEE",
  "channelPreference": ["IN_APP", "EMAIL"],
  "templateFamily": "approval-task-assigned",
  "priority": "HIGH",
  "dedupeWindow": "PT15M",
  "enabled": true
}

Rule harus versioned/effective-dated jika perubahan notification dapat berdampak bisnis.

Jangan hardcode semua rule di consumer class.

Untuk awal project, boleh config static dalam code. Tetapi tetap modelkan konsep rule agar transisi ke admin-governed rules tidak merusak arsitektur.


10. Recipient Resolution

Recipient resolution sering lebih kompleks dari template rendering.

Source recipient:

  • customer contact selected in quote;
  • billing contact;
  • sales owner;
  • sales manager;
  • approval assignee;
  • candidate group;
  • operations queue;
  • tenant admin;
  • external partner endpoint;
  • webhook subscription.

Recipient resolver harus menghasilkan recipient yang eksplisit:

{
  "recipientType": "CUSTOMER_CONTACT",
  "recipientRef": "contact-551",
  "displayName": "Ana Customer",
  "channel": "EMAIL",
  "address": "ana@acme.example",
  "language": "en-US",
  "timezone": "America/New_York"
}

Failure cases:

  • no recipient found;
  • recipient has no valid address;
  • recipient opted out;
  • recipient inactive;
  • duplicate recipient;
  • recipient belongs to another tenant;
  • recipient not authorized for artifact;
  • channel not available for market.

Recipient failure harus menjadi status bisnis, bukan NullPointerException.


11. Preference, Consent, and Suppression

Tidak semua message boleh dikirim hanya karena sistem bisa.

Preference/consent dimension:

  • channel preference;
  • language preference;
  • opt-out;
  • quiet hours;
  • regulatory consent;
  • customer account communication policy;
  • internal role notification policy;
  • bounce suppression;
  • blacklisted domain;
  • tenant-level disable switch;
  • maintenance freeze.

Suppression result harus auditable.

Example:

{
  "communicationId": "com-901",
  "status": "SUPPRESSED",
  "suppressionReason": "CUSTOMER_OPTED_OUT_MARKETING",
  "evaluatedAt": "2026-07-02T10:20:00Z"
}

Namun hati-hati: quote proposal delivery mungkin transactional/contractual, bukan marketing. Preference rule harus membedakan message category.

Jangan memakai satu boolean email_opt_out untuk semua hal.


12. Template and Content Governance

Notification template juga versioned.

Template identity:

quote-proposal-email:2026.07.0
approval-reminder-email:2026.07.0
fallout-alert-inapp:2026.07.0
order-confirmation-email:2026.07.0

Template metadata:

  • family;
  • version;
  • channel;
  • locale;
  • market;
  • message category;
  • owner;
  • effective date;
  • approved by;
  • supported payload schema;
  • subject template;
  • body template;
  • attachment/link policy;
  • retention policy.

Template change can change business meaning.

Example subject change:

Old: Your quote is ready
New: Your approved contract offer is ready

Itu bukan sekadar copywriting. Bisa punya implikasi legal/commercial.


13. Render Data Boundary

Notification render data tidak boleh sembarang mengambil entity penuh.

Bad:

{
  "quote": {"entireQuoteEntity": "..."},
  "customer": {"entireCustomerEntity": "..."}
}

Better:

{
  "quoteNumber": "Q-2026-0001029",
  "customerDisplayName": "Acme Manufacturing",
  "proposalLink": "https://portal.example/quotes/q-10029/artifacts/art-77a",
  "validUntilDisplay": "August 1, 2026",
  "salesContactName": "Dina S."
}

Content rendering harus memakai purpose-built payload.

Keuntungan:

  • mengurangi leakage;
  • template lebih stabil;
  • test lebih mudah;
  • snapshot evidence lebih jelas;
  • compatibility lebih terkontrol.

14. Artifact Attachment Policy

Untuk quote proposal, communication harus terkait artifact.

Ada dua cara umum:

14.1 Attach binary

Email menyertakan PDF attachment.

Pros:

  • mudah untuk customer;
  • offline access;
  • attachment menjadi evidence delivery.

Cons:

  • size limit;
  • forwarded widely;
  • access revocation sulit;
  • duplicate storage in provider;
  • sensitive document leaks easier.

Email berisi link.

Pros:

  • access controlled;
  • can require authentication;
  • can expire;
  • download tracked;
  • easier revocation.

Cons:

  • customer friction;
  • link expiry issue;
  • portal availability dependency.

Policy tergantung business/legal. Untuk high-value B2B quote, portal link sering lebih defensible daripada permanent attachment.

Jika attach PDF, communication record harus menyimpan artifact id dan binary hash yang dikirim.


15. Idempotency and Deduplication

Notification retry bisa menyebabkan duplicate communication.

Idempotency key harus business-specific.

Examples:

approval-task-assigned:{taskId}:{assigneeId}:v1
quote-proposal-delivery:{quoteRevisionId}:{artifactId}:{recipientContactId}:v1
order-confirmation:{orderId}:{recipientContactId}:v1
fallout-alert:{falloutCaseId}:{recipientGroup}:{escalationLevel}:v1

Dedupe window berbeda dari idempotency.

MechanismMeaning
Idempotencysame command returns same communication
Dedupe windowsuppress near-duplicate noisy events
Rate limitbound volume per recipient/channel
Suppressionpolicy says do not send

Jangan hanya mengandalkan provider deduplication.

Provider tidak memahami quote revision, artifact, atau fallout case.


16. Retry Strategy

Retry harus berdasarkan failure class.

FailureRetry?Example
Provider timeoutyesSMTP/API timeout
5xx provideryesprovider unavailable
429 rate limityes with backoffthrottled
Invalid emailnomalformed address
Suppressed recipientnoopted out
Template missingno until fixedconfig error
Artifact unavailablewait/retry via dependencyproposal not generated
Unauthorized artifactnodata policy issue
Webhook 400no or manualreceiver rejected payload
Webhook 500yesreceiver temporary error

Retry policy example:

attempt 1: now
attempt 2: +1 minute
attempt 3: +5 minutes
attempt 4: +30 minutes
attempt 5: +2 hours
then final failure + escalation if critical

Retry must not create new communication record unless business wants resend.

It should create delivery attempts under same communication record.


17. Channel Adapters

Channel adapter hides provider-specific API.

Interface:

public interface DeliveryChannelAdapter {
  DeliveryResult send(DeliveryRequest request);
  Channel channel();
  boolean supports(MessageCategory category);
}

Email adapter request:

{
  "communicationId": "com-901",
  "to": [{"address": "ana@acme.example", "name": "Ana Customer"}],
  "subject": "Your quote Q-2026-0001029 is ready",
  "htmlBody": "...",
  "textBody": "...",
  "attachments": [],
  "headers": {
    "X-Correlation-Id": "corr-abc"
  }
}

Webhook adapter request:

{
  "communicationId": "com-902",
  "endpointId": "crm-main",
  "eventType": "OrderCreated",
  "payload": {...},
  "signature": "..."
}

Provider-specific metadata belongs in attempt record, not in domain-level communication API.


18. Webhook as Communication

Webhook sering dianggap integration, bukan notification. Tapi dalam CPQ/OMS, webhook adalah communication to external system.

Webhook needs:

  • subscription registry;
  • endpoint validation;
  • signing secret;
  • payload schema version;
  • retry policy;
  • idempotency key;
  • event ordering caveat;
  • delivery attempt log;
  • DLQ/manual replay;
  • tenant boundary;
  • rate limit;
  • circuit breaker.

Webhook delivery record:

{
  "communicationType": "EXTERNAL_WEBHOOK",
  "recipientType": "WEBHOOK_SUBSCRIPTION",
  "recipientRef": "sub-120",
  "channel": "WEBHOOK",
  "status": "FAILED_RETRYABLE",
  "subjectType": "ORDER",
  "subjectId": "ord-991"
}

Jangan fire-and-forget webhook tanpa record. Partner akan bertanya, “apakah event sudah dikirim?” Sistem harus bisa menjawab.


19. Camunda 7 Integration

Workflow butuh notification untuk:

  • approval task assignment;
  • approval escalation;
  • document delivery;
  • fallout manual task;
  • customer communication wait states;
  • timeout reminders;
  • external callback.

Pattern yang baik:

Camunda process variable cukup menyimpan:

{
  "communicationId": "com-901",
  "artifactId": "art-77a",
  "quoteId": "q-10029"
}

Jangan simpan rendered email body atau full recipient list di Camunda variable kecuali ada alasan kuat.

Workflow should wait for business-level result only when necessary.

Tidak semua email perlu membuat workflow menunggu delivery. Approval reminder bisa fire-and-observe. Quote proposal delivery mungkin perlu lanjut setelah CommunicationSent atau CommunicationDelivered, tergantung policy.


20. Event Contracts

Notification service consumes events:

QuoteSubmittedForApproval
ApprovalTaskAssigned
ApprovalTaskEscalated
DocumentArtifactAvailable
QuoteAccepted
OrderCreated
OrderFulfillmentFailed
FalloutCaseOpened
OrderCompleted

Notification service publishes events:

CommunicationRequested
CommunicationSuppressed
CommunicationQueued
CommunicationSent
CommunicationFailed
CommunicationBounced
CommunicationDelivered
CommunicationOpened
CommunicationClicked

Event example:

{
  "eventId": "evt-1001",
  "eventType": "CommunicationSent",
  "eventVersion": 1,
  "occurredAt": "2026-07-02T10:20:03Z",
  "tenantId": "tnt-123",
  "communicationId": "com-901",
  "communicationType": "QUOTE_PROPOSAL_DELIVERY",
  "subjectType": "QUOTE_REVISION",
  "subjectId": "qr-004",
  "quoteId": "q-10029",
  "artifactIds": ["art-77a"],
  "channel": "EMAIL",
  "recipientType": "CUSTOMER_CONTACT",
  "recipientRef": "contact-551",
  "correlationId": "corr-abc"
}

Do not publish raw email address unless necessary and allowed.


21. Communication API

Send quote proposal

POST /quotes/{quoteId}/revisions/{revisionNo}/communications/proposal
Idempotency-Key: quote-q-10029-r4-art-77a-contact-551

Body:

{
  "artifactId": "art-77a",
  "recipientContactId": "contact-551",
  "channel": "EMAIL",
  "deliveryMode": "PORTAL_LINK",
  "messageNote": "Optional short sales note"
}

Response:

{
  "communicationId": "com-901",
  "status": "QUEUED",
  "communicationType": "QUOTE_PROPOSAL_DELIVERY"
}

Resend communication

POST /communications/{communicationId}/resend
Idempotency-Key: resend-com-901-20260702-1

Resend should create a new communication record or new attempt depending business meaning.

Recommended:

  • retry same communication for technical failure;
  • resend creates new communication linked to original.

Get communication history

GET /quotes/{quoteId}/communications

Return communication records, not provider logs only.


22. In-App Notification and Worklist Boundary

In-app notification differs from worklist.

SurfaceMeaning
Worklist taskactionable unit user must complete
In-app notificationmessage informing user something happened
Alerthigh-priority issue requiring attention
Dashboard itemaggregate operational state

Approval task assignment creates a worklist task via Camunda/human task model. It may also create an in-app/email notification.

Do not implement approval task as “email only”. Email can be missed. The task must exist in workflow/worklist.

Notification is a prompt, not the task authority.


23. SLA Reminders and Escalations

SLA reminder can be generated by:

  • Camunda timer boundary event;
  • scheduled query over projection;
  • event-driven rule with due time;
  • dedicated SLA service.

Example:

Approval task assigned at 09:00
Reminder at +4 business hours
Escalation at +1 business day
Auto-reassign at +2 business days if policy allows

Notification service should not decide approval escalation policy alone.

It should receive:

ApprovalTaskEscalated

or a command:

SendEscalationNotification

Policy belongs to workflow/policy domain. Notification executes communication.


24. Failure and Fallout

Notification failure itself can create fallout.

Examples:

  • quote proposal delivery final failed;
  • order confirmation email bounced;
  • webhook to billing failed permanently;
  • approver notification suppressed due to missing email;
  • customer contact invalid;
  • artifact unavailable for delivery.

For critical communication, final failure should trigger:

  • operational alert;
  • fallout case;
  • manual retry task;
  • alternative channel;
  • workflow incident/event.

Failure model:

A communication system without failure recovery is not enterprise-grade.


25. Security and Privacy

Notification is a high-risk data leakage point.

Security rules:

  • do not include sensitive data unless necessary;
  • prefer portal links for sensitive artifacts;
  • mask email/phone in logs;
  • store address hash for dedupe/search;
  • encrypt sensitive payload if stored;
  • do not expose provider payload broadly;
  • validate recipient belongs to tenant/subject;
  • prevent mass email injection via user note;
  • sanitize HTML content;
  • sign webhooks;
  • avoid logging signed URLs;
  • separate internal and customer templates;
  • review template variables for leakage.

Example leakage:

Internal margin: 42%
Approval reason: Customer is high risk but acceptable
Cost basis: USD 71,000

This must never appear in customer email.


26. Rate Limiting and Noise Control

Too many notifications create blindness.

Controls:

  • per recipient rate limit;
  • per communication type rate limit;
  • digest mode for low-priority updates;
  • dedupe window;
  • escalation threshold;
  • quiet hours;
  • group notification aggregation;
  • suppress repeated state updates that do not change action.

Example:

Do not email approver more than once every 4 hours for same quote approval task.
Do show in-app task status immediately.
Escalate to manager only after SLA breach.

For operations:

Group 20 similar inventory timeout alerts into one digest if they share root cause.

Notification design must respect human attention as a scarce resource.


27. Content Personalization Boundary

Personalization can be useful but dangerous.

Allowed:

  • recipient name;
  • quote number;
  • sales contact;
  • customer portal link;
  • localized greeting;
  • relevant due date.

Dangerous:

  • exposing internal segmentation;
  • exposing approval score;
  • exposing discount policy threshold;
  • exposing inferred customer risk;
  • dynamic legal wording without template governance;
  • AI-generated customer-facing legal/commercial text without review.

For enterprise CPQ, commercial communication should be deterministic and governed.

AI/dynamic copy may help internal draft, but final customer-facing content must have control points.


28. Bounce, Complaint, and Feedback Handling

Email delivery is not complete when provider accepts message.

Feedback types:

  • hard bounce;
  • soft bounce;
  • complaint/spam report;
  • unsubscribe;
  • open;
  • click;
  • delivery delayed;
  • mailbox full;
  • domain blocked.

Bounce handling:

Provider webhook -> Notification Service -> update attempt/communication -> publish CommunicationBounced -> update contact suppression/read model -> possibly open fallout

Do not let provider webhook directly mutate customer master data without validation.

Bounce event should be interpreted according to channel and message category.


29. Operational Dashboard

Dashboard should show:

  • queued count;
  • sending count;
  • failed retryable count;
  • failed final count;
  • suppressed count;
  • bounce count;
  • provider latency;
  • provider error rate;
  • template failure count;
  • artifact unavailable blockers;
  • communication by type;
  • communication by tenant;
  • delivery SLA breach;
  • retry queue age;
  • webhook endpoint failure rate.

Useful operational questions:

  • Which quote proposals failed to send today?
  • Which customers have bounced order confirmations?
  • Which approvers did not receive task notification?
  • Which template release increased failure rate?
  • Which provider is degraded?
  • Which webhooks are repeatedly failing?

30. Testing Strategy

Testing layers:

TestPurpose
Rule matching testevent maps to expected communication
Recipient resolver testcorrect recipients and failures
Preference testsuppression correctness
Template schema testpayload compatible
Rendering testsubject/body generated safely
Idempotency testduplicate command no duplicate communication
Retry testretryable/non-retryable behavior
Provider adapter contract testrequest/response mapping
Webhook signing testsignature correctness
Authorization testartifact/recipient tenant boundary
Camunda integration testworkflow correlation with communication event
Audit testcommunication trace complete

Scenario test example:

Given quote revision 4 has available artifact art-77a
And customer contact contact-551 has transactional email enabled
When sales sends quote proposal
Then one communication record is created
And it links to artifact art-77a
And it uses template quote-proposal-email active for market/locale
And delivery attempt is queued
And duplicate command with same idempotency key returns same communication

Suppression test example:

Given customer contact has invalid email
When quote proposal delivery is requested
Then communication status becomes FAILED_FINAL or SUPPRESSED according to policy
And no provider attempt is made
And fallout is opened if communication is critical

31. Implementation Skeleton

Command handler:

public final class SendQuoteProposalHandler {
  private final ArtifactClient artifactClient;
  private final RecipientResolver recipientResolver;
  private final PreferenceService preferenceService;
  private final TemplateResolver templateResolver;
  private final MessageRenderer renderer;
  private final CommunicationRepository communicationRepository;
  private final DeliveryQueue deliveryQueue;
  private final OutboxWriter outboxWriter;
  private final AuthorizationService authorizationService;

  public CommunicationResponse handle(SendQuoteProposalCommand command) {
    authorizationService.assertCanSendQuoteProposal(
        command.actor(), command.tenantId(), command.quoteId());

    ArtifactView artifact = artifactClient.getAvailableArtifact(
        command.tenantId(), command.artifactId());

    artifact.assertSubject(command.quoteId(), command.revisionNo());
    artifact.assertType(ArtifactType.QUOTE_PROPOSAL);

    Recipient recipient = recipientResolver.resolveCustomerContact(
        command.tenantId(), command.recipientContactId());

    PreferenceDecision preference = preferenceService.evaluate(
        recipient, MessageCategory.TRANSACTIONAL_QUOTE_DELIVERY);

    TemplateResolution template = templateResolver.resolve(
        "quote-proposal-email",
        recipient.locale(),
        artifact.market(),
        command.requestedAt());

    Communication existing = communicationRepository
        .findByIdempotencyKey(command.tenantId(), command.idempotencyKey())
        .orElse(null);

    if (existing != null) {
      return CommunicationResponse.from(existing);
    }

    Communication communication = Communication.requested(
        command.tenantId(),
        CommunicationType.QUOTE_PROPOSAL_DELIVERY,
        artifact.subject(),
        recipient,
        template,
        command.idempotencyKey(),
        command.actor());

    if (preference.isSuppressed()) {
      communication.markSuppressed(preference.reason());
      communicationRepository.save(communication);
      outboxWriter.append(CommunicationSuppressed.from(communication));
      return CommunicationResponse.from(communication);
    }

    RenderedMessage message = renderer.render(template, QuoteProposalEmailPayload.from(artifact, recipient));
    communication.markRendered(message.hash());
    communication.linkArtifact(artifact.artifactId(), "QUOTE_PROPOSAL");
    communication.markQueued();

    communicationRepository.save(communication);
    deliveryQueue.enqueue(communication.communicationId(), message);
    outboxWriter.append(CommunicationQueued.from(communication));

    return CommunicationResponse.from(communication);
  }
}

Delivery worker:

public final class DeliveryWorker {
  private final DeliveryQueue deliveryQueue;
  private final CommunicationRepository communicationRepository;
  private final DeliveryChannelRegistry channelRegistry;
  private final OutboxWriter outboxWriter;

  public void pollAndSend() {
    deliveryQueue.claimNext().ifPresent(job -> {
      Communication communication = communicationRepository.getForUpdate(job.communicationId());
      DeliveryChannelAdapter adapter = channelRegistry.get(communication.channel());

      DeliveryAttempt attempt = communication.startAttempt();

      try {
        DeliveryResult result = adapter.send(job.toDeliveryRequest(communication));
        communication.applyDeliveryResult(attempt, result);

        if (communication.isSent()) {
          outboxWriter.append(CommunicationSent.from(communication));
        }
        if (communication.isFinalFailure()) {
          outboxWriter.append(CommunicationFailed.from(communication));
        }
      } catch (RetryableDeliveryException e) {
        communication.markAttemptRetryableFailure(attempt, e.code(), e.getMessage());
        deliveryQueue.scheduleRetry(job, e.retryAfter());
      } catch (NonRetryableDeliveryException e) {
        communication.markFinalFailure(attempt, e.code(), e.getMessage());
        outboxWriter.append(CommunicationFailed.from(communication));
      }

      communicationRepository.save(communication);
    });
  }
}

Again: implementation can vary. Lifecycle and invariants must not.


32. Anti-Patterns

32.1 Send email directly from Quote Service

Quote service becomes coupled to channel/provider and loses communication lifecycle.

32.2 Send email directly from Camunda delegate

Workflow execution becomes dependent on email provider latency/failure.

32.3 Treat provider accepted as business delivered

Provider accepted means provider accepted request. It may still bounce later.

32.4 No idempotency key

Retries create duplicate customer emails.

32.5 Store raw email body with sensitive data forever

Audit benefit may not justify privacy risk.

32.6 Use one template for internal and customer communication

Internal variables leak.

32.7 Notification decides business policy

Notification should execute communication, not decide approval or fulfillment logic.

32.8 No operational dashboard

Failed communication becomes invisible until customer complains.


33. Production Readiness Checklist

Before notification service is production-ready:

  • communication record model exists;
  • delivery attempt model exists;
  • idempotency key enforced;
  • retry policy by failure class exists;
  • suppression/preference evaluated;
  • recipient resolution is explicit and testable;
  • template versioning exists;
  • internal/customer template separation exists;
  • artifact link policy exists;
  • sensitive data is masked in logs;
  • provider adapter errors are normalized;
  • webhook signing and retry implemented if webhooks exist;
  • Camunda does not send provider calls directly;
  • critical failure opens fallout/escalation;
  • communication events are published;
  • dashboard shows queue and failure health;
  • bounce/feedback handling exists if email provider supports it;
  • duplicate event handling tested;
  • authorization/tenant boundary tested;
  • runbook exists for provider outage;
  • template release quality gate exists.

34. Mental Model Recap

Notification service di CPQ/OMS enterprise adalah communication ledger + delivery engine.

Ia harus menjawab:

  • apa yang ingin dikomunikasikan;
  • kepada siapa;
  • berdasarkan event/command apa;
  • memakai template versi apa;
  • apakah boleh dikirim;
  • artifact apa yang terkait;
  • apakah sudah dikirim;
  • apakah gagal;
  • apakah perlu retry;
  • apakah perlu escalation;
  • apakah bisa dibuktikan nanti.

Rule paling penting:

Never make critical communication an invisible side effect.

Quote proposal delivery, approval escalation, order confirmation, fallout alert, dan webhook integration adalah bagian dari enterprise lifecycle. Mereka perlu contract, idempotency, observability, audit, dan recovery.

Part berikutnya akan membahas API Composition and Frontend-Facing BFF: bagaimana UI CPQ/OMS mendapatkan view yang cepat dan aman tanpa membuat backend menjadi endpoint chaos atau chatty microservice graph.

Lesson Recap

You just completed lesson 34 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.