Chapter 18 · Change Tracking, CDC, Service Broker, and Integration Patterns

Design an Integration Boundary for CDC, Messaging, ETL, and Cross-System Consistency

Choose deliberately among AGs, replication, log shipping, CDC/ETL and application replication from the information and failover contract actually required.

Advanced190–240 minutesintegration boundary capstoneSQL Server 2025 CU7 · 17.0.4065.4Compatibility 170 · Developer/Express where supportedFree local mandatory path · Last reviewed August 2026

Learning outcomes

The hardest integration failure is often not that a connector is down. It is that every component reports “healthy” while two systems disagree about business state. ServiceHub needs an explicit boundary for propagating work-order changes to analytics and downstream automation. This capstone lesson combines Change Tracking, CDC, Service Broker, ETL and the application outbox pattern into one design process centered on information contracts, failure recovery and reconciliation.

01

Define source of truth, event/change contract, ordering scope, checkpoint semantics and acceptable staleness before selecting a mechanism.

02

Design replay, deduplication and idempotency so retries are safe rather than exceptional.

03

Model poison data, schema evolution, backpressure, retention and consumer outage as normal operating states.

04

Build observability around source position, consumer position, oldest unprocessed item, errors and reconciliation—not process liveness alone.

05

Choose CT, CDC, Service Broker, ETL, outbox or synchronous distributed access deliberately and reject unsuitable combinations.

Lab baseline. SQL Server 2025 (17.x), CU7 build 17.0.4065.4; database compatibility level 170 unless the lesson says otherwise. Use Enterprise Developer or Standard Developer for free non-production learning when the feature is edition-gated. Change Tracking and Service Broker can be explored on Express; boxed SQL Server CDC requires Standard/Enterprise capability and SQL Server Agent. SSMS 22.8.2, VS Code + the current MSSQL extension, or current sqlcmd are supported paths. Azure Data Studio is retired.

1. Start with the information contract

For this design, ServiceHubLab.ops.WorkOrder remains the authoritative operational state. The analytics sink is a derived representation. That single sentence determines how conflicts are resolved: if the warehouse differs from ServiceHub, reconciliation repairs the warehouse from the source contract rather than inventing two-way merge semantics.

Now define what the sink needs. If it only needs the latest current row for every changed key, CT can be sufficient. If it needs every captured insert/update/delete image for incremental ETL, CDC is a better source feed. If the source transaction must enqueue SQL Server-native asynchronous work atomically, Service Broker can fit. If an application needs to publish a domain event to an external broker, an application outbox can atomically store the business change and publishable event in the same database transaction, while a separate relay performs at-least-once delivery. Linked-server writes are a different, synchronous coupling model and should not be smuggled in as a shortcut.

sql · create an outbox/checkpoint/reconciliation learning model
USE ServiceHubLab;GOIF SCHEMA_ID(N'lab18') IS NULL  EXEC(N'CREATE SCHEMA lab18 AUTHORIZATION dbo;');GODROP TABLE IF EXISTS lab18.ConsumerCheckpoint;DROP TABLE IF EXISTS lab18.IntegrationOutbox;GOCREATE TABLE lab18.IntegrationOutbox(  event_id uniqueidentifier NOT NULL PRIMARY KEY,  aggregate_type varchar(40) NOT NULL,  aggregate_id bigint NOT NULL,  event_type varchar(80) NOT NULL,  schema_version smallint NOT NULL,  payload nvarchar(max) NOT NULL,  occurred_at datetime2(3) NOT NULL,  published_at datetime2(3) NULL,  attempt_count int NOT NULL DEFAULT 0,  last_error nvarchar(1000) NULL,  CONSTRAINT CK_IntegrationOutbox_payload_json CHECK (ISJSON(payload)=1));CREATE INDEX IX_IntegrationOutbox_pendingON lab18.IntegrationOutbox(published_at, occurred_at)INCLUDE(event_type, aggregate_id, attempt_count);CREATE TABLE lab18.ConsumerCheckpoint(  consumer_name sysname NOT NULL PRIMARY KEY,  source_kind varchar(20) NOT NULL,  checkpoint_value varbinary(64) NULL,  updated_at datetime2(3) NOT NULL DEFAULT SYSUTCDATETIME(),  status varchar(20) NOT NULL,  last_error nvarchar(1000) NULL);GO

The outbox uses a stable event_id as a deduplication key. schema_version makes payload evolution explicit. A relay is allowed to publish the same event more than once if it crashes after external publish but before setting published_at; therefore consumers must be idempotent or deduplicate by event ID. This is at-least-once delivery made safe, not a false exactly-once claim.

2. Make the business change and outbox record one local transaction

sql · write operational state and its integration event atomically
DECLARE @event_id uniqueidentifier = NEWID();DECLARE @now datetime2(3) = SYSUTCDATETIME();BEGIN TRY  BEGIN TRAN;  UPDATE ops.WorkOrder  SET status = 'onsite'  WHERE work_order_id = 1001;  INSERT lab18.IntegrationOutbox  (event_id,aggregate_type,aggregate_id,event_type,schema_version,payload,occurred_at)  VALUES  (@event_id,'WorkOrder',1001,'WorkOrderStatusChanged',1,   JSON_OBJECT('workOrderId':1001,'status':'onsite'),@now);  COMMIT;END TRYBEGIN CATCH  IF XACT_STATE() <> 0 ROLLBACK;  THROW;END CATCH;GOSELECT event_id, aggregate_id, event_type, schema_version,       occurred_at, published_at, attempt_countFROM lab18.IntegrationOutboxORDER BY occurred_at;GO

The local database transaction now guarantees that the operational update and the durable intent-to-publish appear together. It does not guarantee external delivery. The relay owns that second boundary, including backoff, deduplication, dead-letter/poison policy and observability.

Wrong approach. Updating the work order, committing, then directly calling an external HTTP/broker API from application code creates a dual-write window. If SQL succeeds and publish fails, the systems diverge. If publish succeeds and the process crashes before recording success, retry can duplicate. The outbox pattern does not eliminate failure; it moves failure into a replayable state that can be observed and reconciled.

3. Design ordering, replay, deduplication and schema evolution

Global ordering is expensive and often unnecessary. Define the scope: ServiceHub may require status events for the same work order to be applied in order, while events for unrelated work orders can be processed concurrently. A consumer can track the highest logical version per aggregate or use event sequence metadata. If ordering is derived from database LSNs or CT versions, document the precise scope and never reinterpret those engine positions as business clocks across databases.

Replay needs a starting point and a validity test. CT uses a client version and minimum-valid-version check. CDC uses low/high LSN endpoints and consumer checkpoints. Service Broker uses durable queues and conversation endpoints. An outbox uses event IDs plus relay state. Each mechanism has a retention boundary. If the consumer falls behind beyond that boundary, the correct behavior is a full snapshot/rebuild or another explicitly tested recovery path.

sql · observe backlog age and retry pressure, not just relay liveness
SELECT  COUNT(*) AS pending_events,  MIN(occurred_at) AS oldest_pending_at,  MAX(attempt_count) AS max_attempt_count,  DATEDIFF(second, MIN(occurred_at), SYSUTCDATETIME()) AS oldest_pending_age_secondsFROM lab18.IntegrationOutboxWHERE published_at IS NULL;SELECT TOP (20)  event_id, aggregate_id, event_type, schema_version,  occurred_at, attempt_count, last_errorFROM lab18.IntegrationOutboxWHERE published_at IS NULLORDER BY occurred_at;GO

A process heartbeat can stay green while oldest_pending_age_seconds grows for hours. Backlog age, checkpoint lag, retry counts and reconciliation discrepancies are stronger integration health signals than “the connector process is running.”

4. Reconciliation is the safety net for every asynchronous design

Even with careful checkpoints, integration systems need reconciliation. Compare source and target counts/hashes or business invariants over stable partitions, sample key states, verify high-water marks, and preserve mismatch evidence. Reconciliation should be able to classify a mismatch as “target behind,” “duplicate,” “schema rejected,” “source corrected,” or “unknown” rather than immediately overwriting data.

sql · store reconciliation evidence in the source lab
DROP TABLE IF EXISTS lab18.ReconciliationRun;GOCREATE TABLE lab18.ReconciliationRun(  run_id bigint IDENTITY PRIMARY KEY,  scope_name varchar(80) NOT NULL,  source_rows bigint NOT NULL,  target_rows bigint NOT NULL,  mismatch_rows bigint NOT NULL,  source_checkpoint nvarchar(100) NULL,  target_checkpoint nvarchar(100) NULL,  recorded_at datetime2(3) NOT NULL DEFAULT SYSUTCDATETIME());INSERT lab18.ReconciliationRun(scope_name,source_rows,target_rows,mismatch_rows,source_checkpoint,target_checkpoint)VALUES ('work-orders-demo',4,4,0,N'ct-version:demo',N'sink-version:demo');SELECT * FROM lab18.ReconciliationRun ORDER BY run_id DESC;GO

The numbers are explicitly demonstration records, not measurements of a real external system. A production reconciler should obtain evidence from both sides, define a consistency cut, and store enough context to repeat or audit the decision.

5. Choose the mechanism from the contract

Need Likely fit Critical boundary
Latest rows changed since client checkpoint Change Tracking Retention validity; no full before/after history
Relational insert/update/delete history for incremental ETL CDC Agent/capture lag, LSN retention, consumer checkpoint
Transactional SQL-native asynchronous work Service Broker Conversation/queue semantics, poison handling, topology
External event bus with atomic source change intent Application outbox + relay At-least-once publish, dedupe, schema/version ownership
Batch transformation into another model ETL/ELT using CT/CDC/watermarks Idempotent load, checkpoints, replay, reconciliation
Synchronous remote query/write Linked server only when coupling is acceptable Latency, provider/security, remote transaction failure domain

These are starting points, not a product scoreboard. A design can combine mechanisms—for example CDC as a source feed into an ETL platform, or an outbox relay that publishes to an external broker. Each additional component adds failure states. State ownership and recovery must remain explicit.

sql · chapter cleanup: remove disposable integration objects carefully
USE ServiceHubLab;GO-- Lab-only Service Broker cleanup. WITH CLEANUP is appropriate here only because-- these conversations belong to this disposable course namespace.DECLARE @lab_dialog uniqueidentifier;WHILE 1=1BEGIN  SET @lab_dialog = NULL;  SELECT TOP (1) @lab_dialog = conversation_handle  FROM sys.conversation_endpoints  WHERE far_service LIKE N'//ServiceHub/Lab18/%';  IF @lab_dialog IS NULL BREAK;  END CONVERSATION @lab_dialog WITH CLEANUP;END;IF EXISTS (SELECT 1 FROM sys.services WHERE name=N'//ServiceHub/Lab18/Initiator')  DROP SERVICE [//ServiceHub/Lab18/Initiator];IF EXISTS (SELECT 1 FROM sys.services WHERE name=N'//ServiceHub/Lab18/Target')  DROP SERVICE [//ServiceHub/Lab18/Target];IF OBJECT_ID(N'lab18.InitiatorQueue',N'SQ') IS NOT NULL DROP QUEUE lab18.InitiatorQueue;IF OBJECT_ID(N'lab18.TargetQueue',N'SQ') IS NOT NULL DROP QUEUE lab18.TargetQueue;IF EXISTS (SELECT 1 FROM sys.service_contracts WHERE name=N'//ServiceHub/Lab18/WorkContract')  DROP CONTRACT [//ServiceHub/Lab18/WorkContract];IF EXISTS (SELECT 1 FROM sys.service_message_types WHERE name=N'//ServiceHub/Lab18/WorkRequested')  DROP MESSAGE TYPE [//ServiceHub/Lab18/WorkRequested];GO-- Keep ops.WorkOrder and all course baseline objects.DROP TABLE IF EXISTS lab18.ReconciliationRun;DROP TABLE IF EXISTS lab18.ConsumerCheckpoint;DROP TABLE IF EXISTS lab18.IntegrationOutbox;DROP TABLE IF EXISTS lab18.RemoteLatencyEvidence;GO-- Optional cleanup only if Lesson 2 CDC objects were created.IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id=OBJECT_ID(N'lab18.CdcWorkOrder') AND is_tracked_by_cdc=1)BEGIN  EXEC sys.sp_cdc_disable_table       @source_schema=N'lab18',       @source_name=N'CdcWorkOrder',       @capture_instance=N'lab18_CdcWorkOrder';END;DROP TABLE IF EXISTS lab18.CdcWorkOrder;GO-- Optional cleanup only if Lesson 1 Change Tracking table exists.IF OBJECT_ID(N'lab18.SyncItem',N'U') IS NOT NULLBEGIN  IF EXISTS (SELECT 1 FROM sys.change_tracking_tables WHERE object_id=OBJECT_ID(N'lab18.SyncItem'))    ALTER TABLE lab18.SyncItem DISABLE CHANGE_TRACKING;  DROP TABLE lab18.SyncItem;END;GO-- Do not disable database-wide CDC or Change Tracking here automatically:-- another lab/application could be using those database options.-- Review dependencies before changing database-wide integration settings.

Cleanup deliberately avoids disabling database-wide CDC or Change Tracking automatically. Database-wide feature state can be shared by multiple tables; a safe operator inventories dependencies before changing it. The same principle applies to Service Broker objects and linked servers: remove only what the disposable lab owns.

6. Production judgment and bridge

A robust integration boundary names the authoritative system, payload/change semantics, ordering scope, delivery guarantee, checkpoint, retention, replay, deduplication, poison handling, schema compatibility, backpressure policy, observability and reconciliation owner. “Connector running” is implementation status, not consistency evidence. Chapter 19 moves from data movement to analytical storage and execution, where columnstore rowgroups, segment elimination and batch mode change how SQL Server serves large analytical workloads.

Check your understanding

  1. Why does an outbox normally provide at-least-once rather than exactly-once external delivery?
  2. What is a stronger health metric than “connector process is running”?
  3. When should CT be rejected in favor of CDC or another history mechanism?
  4. Why must schema evolution be in the integration contract?
  5. What should happen when a consumer checkpoint falls outside retained source history?
Review the answers

1. The external publish and source-row update are separate commits; a crash between them can cause replay, so stable event IDs and idempotent consumers are required.

2. Oldest unprocessed age, checkpoint lag, retry/error counts, retention headroom and reconciliation mismatches directly reflect integration progress.

3. When downstream consumers require every intermediate change or before/after row images rather than only which keys changed and their current state.

4. Consumers may lag deployments or replay old events; explicit versions and compatibility rules keep producers from silently breaking them.

5. Use the tested re-bootstrap/full-snapshot path; never interpret missing retained history as “nothing changed.”

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.