Back to Work

RAW TO RELIABLE / ENGINEERING CASE STUDY / CMS MEDICARE

CMS Medicare Streaming Data Pipeline

Kafka and Spark Structured Streaming process public Medicare hospital charge records into Snowflake snapshots and dbt models for DRG charge-to-payment analysis.

Streaming and dbt code implemented; Airflow orchestration documented separately. Periodically published public records are replayed through a stateful processing pipeline.

Project stateCompleted

Focus areas

  • Kafka ingestion
  • Stateful Spark micro-batches
  • Snowflake aggregate snapshots
  • DRG outlier and severity rules
  • Documented Airflow workflow

Technology stack

KafkaSpark Structured StreamingApache AirflowSnowflakedbtPythonDockerSQL

01 / Executive Overview

Executive Overview

Make public hospital charge and payment differences easier to investigate.

Business question

Compare charges with payments

How do submitted hospital charges compare with recorded Medicare payments across DRGs? The pipeline makes these differences queryable for analytical investigation.

Source grain

Provider × DRG, within a release

CMS publishes provider/service summaries for a reporting year. They describe public hospital-level aggregates, not patient-level claims or a live event feed.

Processing

Key, aggregate, refresh

Python pages through the API, Kafka carries DRG-keyed records, and Spark produces complete aggregate snapshots through foreachBatch.

Analytical output

Rules and ranks at DRG-group grain

dbt filters and renames aggregate fields, adds a statistical outlier flag and separate severity bands, then ranks ratios within each band.

Submitted charge
The source's average covered hospital charge per DRG discharge; a billed amount, not an estimate of cost.
Medicare payment
The source's average Medicare payment measure. Total payment is retained separately and includes other payment components.
Charge ratio
Spark averages each input row's charge/payment ratio. A high ratio is an analytical signal, not hospital profit or confirmed misconduct.
dbt models
03
Declared dbt tests
09
Processing-time trigger
30s

The trigger is a configured processing cadence, not a measured end-to-end latency. CMS refreshes this public dataset annually; the project simulates streaming by sending retrieved records through Kafka.

02 / Streaming Architecture

Streaming Architecture

An implemented streaming data path, with documented orchestration kept visibly separate.

CMS / STREAMING DATA & CONTROL PATHS / FIG. 01
  • Source-defined movement / dependency
  • Documented / manual relationship
  • Orchestration / control

Airflow / README-described control workflow / DAG not published

  1. 01 / Documented task

    Produce

    Invocation unverified

  2. Doc.

    02 / Documented task

    Stream

    Invocation unverified

  3. Doc.

    03 / Documented task

    dbt run

    Invocation unverified

  4. Doc.

    04 / Documented task

    dbt test

    Invocation unverified

Ingestion and aggregation / source-defined operations

  1. 01 / Source

    CMS API

    Periodic public records

  2. HTTP

    02 / Acquisition

    Python producer

    size / offset pages

  3. Send

    03 / Messages

    Kafka topic

    cms-inpatient-stream

  4. Read

    04 / Processing

    Spark stream

    DRG code + description

Snapshot sink / continues from Spark aggregates

  1. 01 / State

    Complete output

    All current DRG groups

  2. Batch

    02 / Callback

    foreachBatch

    collect() to driver

  3. Call

    03 / Replace

    Python writer

    TRUNCATE → INSERT

  4. Write

    04 / Snapshot

    Snowflake

    DRG_CHARGE_ANALYSIS

dbt analytical path / separately invoked model build

  1. 01 / Source

    Snapshot table

    Latest stored aggregates

  2. source()

    02 / Normalize

    Staging view

    Rename / filter

  3. ref()

    03 / Classify

    Intermediate view

    Outlier / severity

  4. ref()

    04 / Analyze

    Mart table

    Rank within severity

Solid arrows show implemented code dependencies and data operations. Repeated Spark and Snowflake boundary nodes continue the flow between rows. Dashed Airflow edges are the README-described workflow: its DAG is not published, and a completed end-to-end scheduled run is not established.

Records become a refreshed snapshot

Spark groups by DRG code and description, emits complete aggregate state, and calls a Python Snowflake writer. The sink replaces its analytical snapshot; it does not retain an append-only event history.

dbt runs against stored aggregates

Staging and intermediate views depend on the current snapshot. The mart is a separate table build. Airflow is shown above the data flow as documented control, rather than as a downstream store.

03 / Kafka Ingestion & Partitioning

Kafka Ingestion & Partitioning

Page through a public dataset, then publish full rows with a DRG routing key.

  1. 01 / RetrieveCMS API pages

    Use size and offset; request up to 1,000 rows per page, stopping at the 5,000-record target or an empty response.

  2. 02 / SerializeUTF-8 JSON

    Publish each complete source row. DRG_Cd becomes the key; an absent field defaults to UNKNOWN.

  3. 03 / Publishcms-inpatient-stream

    Send the page of messages and flush the producer. A half-second sleep between pages simulates arrival gaps.

  4. 04 / ConsumeSpark Kafka source

    Read the topic from earliest offsets for a new query, then parse the payload into the selected schema.

Actual producer limits / development target, not an ingestion result
BATCH_SIZE = 1000 # How many rows importing from API
TOTAL_ROWS = 5000 # Verify after first 5000 rows, full importing 145000 later on
Actual keyed publishing / DRG_Cd as the message key
        key = row.get("DRG_Cd", "UNKNOWN")
        producer.send(TOPIC, key=key, value=row)
    producer.flush()
CMS / CONCEPTUAL KEYED ROUTING / FIG. 02

Producer key

DRG_Cd

UTF-8 key bytes

  1. Illustrative target

    Partition 0

    Actual assigned keys unverified

  2. Illustrative target

    Partition 1

    Actual assigned keys unverified

  3. Illustrative target

    Partition 2

    Actual assigned keys unverified

Illustrative routing structure only. Three partitions are documented in the README’s topic-creation command; no broker metadata or exact key-to-partition assignments are recorded. A stable key and partition configuration route repeated keys consistently. This does not establish global ordering or eliminate Spark aggregation shuffles.

Source GUID: 690ddc6c-2767-4618-b277-420ffb2bf27c. A read-only API audit on October 14, 2026 reported 145,879 available rows; catalog metadata identifies an annual release covering 2024. This source count is separate from the producer target and does not prove how many records reached Kafka.

04 / Spark Structured Streaming

Spark Structured Streaming

Stateful DRG aggregation with complete output every configured micro-batch.

CMS / SPARK MICRO-BATCH PROCESSING / FIG. 03
  • Source-defined movement / dependency
  • Documented / manual relationship
  • Orchestration / control

Spark 3.5.1 Kafka connector / local[*] execution

  1. 01 / Read

    Kafka records

    startingOffsets: earliest

  2. JSON

    02 / Normalize

    Parse & cast

    11 JSON fields

  3. Group

    03 / Aggregate

    Stateful group

    DRG_Cd / DRG_Desc

  4. Emit

    04 / Snapshot

    Complete output

    30-second trigger

JSON records are parsed into eleven selected string fields, then charge/payment values are cast to FloatType and discharge counts to integers. Group state accumulates by DRG_Cd and DRG_Desc. Complete output passes all current aggregate groups to the writer on a 30-second processing-time trigger.
hospital_count
COUNT(*)

Counts input messages in each DRG code/description group; not distinct provider CCNs.

avg_charge / avg_medicare_pymt / avg_total_pymt
ROUND(AVG(field), 2)

Unweighted averages of the provider/DRG summary records received by Spark.

avg_charge_ratio
ROUND(AVG(charge / medicare_payment), 2)

Average of individual source-row ratios, not AVG(charge) / AVG(payment). Discharges are cast but not used as weights.

Actual ratio expression / average of individual input-row ratios
            spark_round(
                avg(col("charge") / col("medicare_payment")), 2
            ).alias("avg_charge_ratio"),
Actual output mode, callback, and trigger
        drg_agg.writeStream
        .outputMode("complete")
        .foreachBatch(write_to_snowflake)
        .trigger(processingTime="30 seconds")
        .start()

State and replay need explicit contracts

There is no configured durable checkpoint location, deduplication key, event-time window, or watermark. Replayed records can change counts and means. The code does not establish exactly-once writes or bounded state across changing DRG descriptions.

Validate before dividing

The stream has no explicit positive-payment filter, zero-denominator guard, or quarantine before averaging. Invalid casts and malformed records need validation; their actual runtime behavior is not recorded. count(*) also counts messages rather than distinct providers.

05 / Snowflake & dbt Data Modeling

Snowflake & dbt Data Modeling

Replace an aggregate snapshot, then build three explicit analytical layers.

The foreachBatch callback collects aggregate rows to the driver and uses snowflake-connector-python. For a non-empty batch it truncates DRG_CHARGE_ANALYSIS, inserts the current aggregate rows, commits, and closes the connection. Empty batches leave the stored snapshot untouched.

Selected writer operations / insert definition and tuple mapping omitted
        cursor.execute("TRUNCATE TABLE STREAMING.DRG_CHARGE_ANALYSIS")

        # ... INSERT statement and row tuples omitted ...

        cursor.executemany(insert_sql, data)
        conn.commit()

Snapshot replacement has a failure boundary

No protective staging/swap, batch ledger, rollback strategy, or explicit transaction configuration is present. A failure between truncate and reload can affect availability or expose an incomplete snapshot. This is a design risk, not a reported outage or a transactional guarantee.

Future production alternatives

A staging table plus atomic replacement, key-based MERGE, versioned snapshots, or a checkpointed incremental sink would address different replay and history requirements. These are future alternatives, not implemented features.

CMS / SNOWFLAKE & DBT MODEL FLOW / FIG. 04
  • Source-defined movement / dependency
  • Documented / manual relationship
  • Orchestration / control

Streaming snapshot → staging → intermediate → mart

  1. 01 / Snowflake

    Streaming table

    Streaming aggregate snapshot

  2. source()

    02 / View

    Staging

    Normalize / positive means

  3. ref()

    03 / View

    Intermediate

    Global statistics / flags

  4. ref()

    04 / Table

    Analytical mart

    Severity-specific ranks

A model dependency flow, not a star schema. Spark’s source grain is DRG code plus description; the SQL layers retain those grouped rows. Unique DRG-code declarations test an expected grain but do not prove one row per code without test output.

Staging / view

stg_drg_charge_analysis

Renames fields; retains non-null DRG codes and positive aggregate submitted-charge/payment means. It does not regroup, deduplicate, or validate each original input row.

Intermediate / view

int_drg_outlier_flagged

CROSS JOINs one global statistics row, calculates the charge gap, and attaches outlier and severity classifications without changing the underlying group grain.

Mart / table

mart_drg_spending_quality

Preserves the classified rows and RANKs charge ratios within severity. Ties share a rank and can leave gaps; the stored table refreshes only when dbt is run.

The snapshot table supplies updated_at through a Snowflake default, renamed batch_updated_at in staging. The mart preserves that snapshot timestamp; no historical version key or time-series accumulation is implemented.

06 / Data Quality & Anomaly Rules

Data Quality & Anomaly Rules

Keep statistical outliers, rule-based severity, and test evidence distinct.

Statistical flag

Above mean + 2 × standard deviation

The intermediate model calculates AVG and STDDEV across staged DRG-group ratios. A strict greater-than comparison marks a high-side analytical outlier; it is not a causal or fraud finding.

Rule-based severity

Ordered charge-ratio bands

Critical, High, Medium, and Normal are project-defined labels. They are separate from the statistical flag and are not CMS-endorsed regulatory classifications. The charge gap is a charge/payment difference, not profit.

CMS / SOURCE-DEFINED SEVERITY BANDS / FIG. 05
  1. Critical

    ratio ≥ 15

    First CASE branch

  2. High

    10 ≤ ratio < 15

    Second CASE branch

  3. Medium

    5 ≤ ratio < 10

    Third CASE branch

  4. Normal

    ratio < 5 / ELSE

    NULL also falls through to ELSE

Ordered CASE branches from the intermediate SQL. The first matching threshold wins; ELSE also classifies a NULL ratio as Normal. A not_null ratio test is declared separately. No sample KPI values or counts of flagged DRGs are asserted.
Selected intermediate SQL / independent outlier and severity CASE expressions
        CASE
            WHEN b.avg_charge_ratio > (s.mean_ratio + 2 * s.stddev_ratio)
            THEN TRUE
            ELSE FALSE
        END                             AS is_charge_outlier,

        CASE
            WHEN b.avg_charge_ratio >= 15 THEN 'Critical'
            WHEN b.avg_charge_ratio >= 10 THEN 'High'
            WHEN b.avg_charge_ratio >= 5 THEN 'Medium'
            ELSE 'Normal'
        END                             AS charge_severity

not_null

6 declarations

Staging: DRG code, submitted-charge mean, Medicare-payment mean, and ratio. Mart: DRG code and severity.

unique

2 declarations

DRG code in staging and mart. These test the expected code grain; they do not prove it without execution results.

accepted_values

1 declaration

Mart severity must be Critical, High, Medium, or Normal. No relationship tests or intermediate-model tests are declared.

Nine tests are defined: five on staging and four on the mart. The README reports nine tests with zero failures, but no machine-readable result or execution log independently verifies that result. No fake pass indicator or sample financial table is reproduced.

07 / Apache Airflow Orchestration

Apache Airflow Orchestration

Present the documented task chain alongside the orchestration code actually published.

CMS / DOCUMENTED AIRFLOW WORKFLOW / FIG. 06
  • Source-defined movement / dependency
  • Documented / manual relationship
  • Orchestration / control

cms_medicare_pipeline / README-described DAG, code not published

  1. 01 / README task

    run_cms_producer

    Retrieve API records and publish Kafka messages.

  2. Doc.

    02 / README task

    run_spark_streaming

    Start Spark processing and Snowflake snapshot writes.

  3. Doc.

    03 / README task

    run_dbt_models

    Build the staging/intermediate views and mart table.

  4. Doc.

    04 / README task

    run_dbt_tests

    Run the nine declared model checks.

All dashed edges are documented dependencies. The README specifies these four task IDs and a daily 06:00 UTC schedule; the published revision contains no DAG file to verify the schedule, timezone, retries, parameters, execution bounds, or successful downstream completion.
  1. 01 / Documented task

    run_cms_producer

    Retrieve API records and publish Kafka messages.

    Implementation boundary
    Producer code is present; the Airflow invocation is documented only.
  2. 02 / Documented task

    run_spark_streaming

    Start Spark processing and Snowflake snapshot writes.

    Implementation boundary
    The job awaits termination indefinitely; the DAG's stop/completion behavior is not published.
  3. 03 / Documented task

    run_dbt_models

    Build the staging/intermediate views and mart table.

    Implementation boundary
    The documented dependency requires the preceding task to complete.
  4. 04 / Documented task

    run_dbt_tests

    Run the nine declared model checks.

    Implementation boundary
    No task logs or test execution artifact establish successful completion.

Streaming lifetime affects downstream tasks

The checked-in Spark job calls awaitTermination() and has no available-now trigger, timeout, or programmed stop. A synchronous Airflow invocation of that job would block downstream dbt tasks until the stream stops or fails. The missing DAG prevents verifying how the documented workflow handles this boundary.

Compose shows cross-container intent

The published Airflow stack defines LocalExecutor, PostgreSQL metadata storage, a scheduler/webserver, an external CMS Docker network, and producer/dbt mounts. It also mounts a Docker socket. Historical mount paths, the missing DAG/profile, and the absent Kafka/Spark Compose file prevent a direct replay of the documented environment.

08 / Engineering Decisions & Troubleshooting

Engineering Decisions & Troubleshooting

Explain routing, runtime, sink, and container contracts with implementation and author-reported lessons separated.

Source + documentation

Use the DRG code as the message key

Observed problem
Provider records for the same DRG need a stable routing key.
Root cause
A diagnosis group is shared by records from multiple providers.
Engineering solution
Serialize DRG_Cd as the UTF-8 Kafka key and each source row as JSON.
Validation / result
The producer implements keyed publishing. Stable key routing does not establish global ordering or remove Spark aggregation shuffles; no physical execution plan is published.

Source evidence

Source + documentation

Keep Spark's Kafka dependency explicit

Observed problem
Spark needs a compatible connector to read the Kafka source.
Root cause
Kafka support depends on a matching Spark/Scala connector package.
Engineering solution
Declare spark-sql-kafka-0-10_2.12:3.5.1 in SparkSession configuration.
Validation / result
The source requests that package and runs local[*]. Cluster deployment, dependency-resolution logs, and achieved throughput are not claimed.

Source evidence

Source + documentation

Write complete aggregates with a Python callback

Observed problem
The README describes native-connector JAR compatibility friction.
Root cause
The chosen implementation uses a Python driver callback rather than the native Spark Snowflake connector.
Engineering solution
Use foreachBatch, collect the aggregate rows, then TRUNCATE and bulk insert with snowflake-connector-python.
Validation / result
The callback is implemented. Driver collection and sequential snapshot replacement trade simplicity for memory, replay, and visibility constraints; an atomic replacement mechanism is not present.

Source evidence

Documented decision

Separate bootstrap reachability from broker metadata

Observed problem
The author's CMS networking article reports NoBrokersAvailable across Compose projects.
Root cause
A bootstrap connection and the advertised addresses used afterward both need to be reachable.
Engineering solution
The documented setup adds a DOCKER listener at kafka:39092; published Airflow Compose also joins the external CMS network.
Validation / result
The article records a successful connection after its change. Current broker configuration and runtime logs are absent, so the three-listener deployment is not independently verified.

Source evidence

Documented decision

Make container lifetime a foreground-process contract

Observed problem
The author's CMS Spark note describes containers exiting after daemon startup scripts returned.
Root cause
The container lifecycle follows its main process, while a daemon launcher can exit after starting a child.
Engineering solution
The note compares foreground execution and local-development keepalive patterns.
Validation / result
This is a documented troubleshooting lesson. The CMS Kafka/Spark Compose file is not published, so no particular remedy is asserted as the checked-in deployment.

Source evidence

CMS / KAFKA NETWORK CONTEXTS / FIG. 07
INTERNAL / 29092
kafka:29092

Spark’s configured client target; same-network broker advertisement is documented.

EXTERNAL / 9092
localhost:9092

Producer’s default host context. Inside a container, localhost refers to that container.

DOCKER / 39092
kafka:39092

Author-documented cross-project target; current broker listener and DAG invocation are unverified.

Client targets are visible in producer/Spark code or documented in the linked networking article. The broker’s three-listener configuration is not checked in. Bootstrap access alone does not verify that addresses returned in broker metadata are reachable.

09 / Evidence, Limitations & Source Code

Evidence, Limitations & Source Code

A code-grounded streaming walkthrough with an explicit boundary around execution claims.

Verified in the published source

API pagination and producer limits; DRG-keyed JSON messages; Spark schema, grouping, complete output, trigger and callback; snapshot replacement; three dbt layers; nine test definitions; and the Airflow Compose/Dockerfile. No broker, Spark, Snowflake, dbt, or Airflow service was executed during this audit.

Read-only source observations

The public CMS API and catalog were checked separately for schema, available source count, annual refresh, and reporting-year context. These observations verify public source availability, not successful ingestion, exact provider/DRG uniqueness, or end-to-end pipeline execution.

Documented, not independently executed

Three topic partitions, three broker listeners, a daily Airflow schedule, the named DAG chain, the nine-tests-passed statement, and the related troubleshooting narratives. No DAG, Kafka/Spark Compose, physical plan, checkpoint configuration, test result file, or historical snapshot output is published.

Published paths need alignment

The repository uses Kafka/, Spark/, Models/, and dbt/, while Compose and README reference historical CMS_project/cms_dbt and DOT_project layouts. The dbt configuration expects models/ under its project; model SQL is actually in top-level Models/. No successful clean-checkout replay is established.

Unverified sample financial results and the README graphic’s full-run/pass labels were omitted. No patient data, source account identifiers, passwords, or private endpoints were copied. The snapshot sink does not establish exactly-once writes, complete history, observed availability, measured latency, or production operation.

Source reviewed at 5143bdc. The Kafka partition figure is illustrative; the other figures explain source-defined code or clearly labeled documented workflows. Charge severity and statistical outliers require business context before any operational decision.