Jaco van der Laan

Source Delivery Properties: When Is My Data Actually Ready?

10/03/2026  ·  Data Architecture  ·  Data Delivery

‹ ›

Modeling source-specific characteristics for intelligent integration layer orchestration


Source Delivery Properties: When Is My Data Actually Ready?

Modeling source-specific characteristics for intelligent integration layer orchestration


The EOM Snapshot Problem

It’s the last working day of the month. Your finance team needs the end-of-month snapshot processed by 6 AM. But here’s the challenge:

  • GL Transactions delivers daily, including weekends
  • FX Rates only delivers on working days
  • Positions delivers weekdays, but Friday’s file contains Thursday’s data
  • Market Data is optional enrichment — nice to have, but don’t wait for it

Your integration pipeline has dependencies on all four sources. When can it actually start?

This is the source delivery properties problem. Every source system has unique delivery characteristics, and your orchestration needs to understand them.


Beyond Simple Dependencies

Modern orchestration tools handle dependencies well. “Wait for upstream_pipeline to succeed before starting downstream_pipeline.” Simple AND/OR logic.

But source data is more nuanced:

Source Delivery Characteristics:  
┌─────────────────────────────────────────────────────────────────┐  
│                                                                 │  
│  GL_TRANSACTIONS          FX_RATES             POSITIONS        │  
│  ┌─────────────┐          ┌─────────────┐      ┌─────────────┐  │  
│  │ Daily       │          │ Weekdays    │      │ Weekdays    │  │  
│  │ T+0         │          │ T+0         │      │ T+1         │  │  
│  │ Mandatory   │          │ Mandatory   │      │ Mandatory   │  │  
│  └─────────────┘          └─────────────┘      └─────────────┘  │  
│                                                                 │  
│  MARKET_DATA              COUNTERPARTY_RATINGS                  │  
│  ┌─────────────┐          ┌─────────────┐                       │  
│  │ Daily       │          │ Weekly      │                       │  
│  │ T+0         │          │ T+7 OK      │                       │  
│  │ Optional    │          │ Enrichment  │                       │  
│  └─────────────┘          └─────────────┘                       │  
│                                                                 │  
└─────────────────────────────────────────────────────────────────┘

Each source has:

  1. Delivery schedule — When does data arrive?
  2. Reference date — What date does the data represent?
  3. Dependency class — Must we wait for it?
  4. Freshness tolerance — How stale can it be?

The Three Dependency Classes

Not all sources are created equal. We classify them:

Mandatory Sources

Definition : Processing cannot begin without this data.

source:  
  entity_id: ENT_gl_transactions  
  dependency_class: mandatory  
  delivery_schedule: daily  
  max_age_hours: 24

If GL transactions aren’t present for the snapshot period, we don’t start. Period.

Optional Sources

Definition : Process with available data, but don’t block if missing.

source:  
  entity_id: ENT_market_data  
  dependency_class: optional  
  delivery_schedule: daily  
  max_age_hours: 48  
  staleness_warning_hours: 12

Market data enriches our output, but yesterday’s prices are acceptable if today’s aren’t available.

Enrichment Sources

Definition : Nice to have, never blocks processing.

source:  
  entity_id: ENT_counterparty_ratings  
  dependency_class: enrichment  
  delivery_schedule: weekly  
  staleness_warning_hours: 168  # 1 week

Credit ratings update weekly. We use whatever’s available — even month-old data won’t block processing.


The Reference Date Problem

Here’s where it gets interesting. When we ask “is data complete for March 31st?”, we need to understand what each source means by “March 31st data.”

Calendar Day vs Working Day

March 2026:  
Sun Mon Tue Wed Thu Fri Sat  
 29  30  31   1   2   3   4  
          ▲  
          └── March 31st is a Tuesday

For sources delivering on working days:  
  - "Last working day of March" = March 31st ✓  
But what if March 31st were a Sunday?  
  - Calendar day: Expect data for March 31st  
  - Last working day: Expect data for March 27th (Friday)

T+0 vs T+1 Delivery

Some sources deliver today’s data today (T+0). Others deliver yesterday’s data today (T+1).

# FX Rates: T+0 delivery  
# File received March 31st contains March 31st rates  
reference_date = snapshot_date  # Same day

# Positions: T+1 delivery  
# File received March 31st contains March 30th positions  
reference_date = snapshot_date - timedelta(days=1)

For an EOM snapshot, the Positions source on March 31st contains March 30th data. We actually need to wait until April 1st to get March 31st positions.


Business Calendar Integration

Working day calculations require business calendars. Different regions have different holidays.

from dataclasses import dataclass  
from datetime import date, timedelta  
from typing import List, Set

@dataclass  
class BusinessCalendar:  
    """Business calendar with holidays."""  
    calendar_id: str  
    working_days: List[int] = None  # 0=Mon, 4=Fri  
    holidays: Set[date] = None  
    def __post_init__(self):  
        self.working_days = self.working_days or [0, 1, 2, 3, 4]  
        self.holidays = self.holidays or set()  
    def is_working_day(self, d: date) -> bool:  
        if d.weekday() not in self.working_days:  
            return False  
        return d not in self.holidays  
    def last_working_day(self, d: date) -> date:  
        while not self.is_working_day(d):  
            d = d - timedelta(days=1)  
        return d  
    def last_working_day_of_month(self, year: int, month: int) -> date:  
        if month == 12:  
            last = date(year + 1, 1, 1) - timedelta(days=1)  
        else:  
            last = date(year, month + 1, 1) - timedelta(days=1)  
        return self.last_working_day(last)

Now we can calculate reference dates correctly:

# Netherlands calendar with public holidays  
nl_calendar = BusinessCalendar(  
    calendar_id="NL",  
    holidays={  
        date(2026, 4, 27),  # King's Day  
        date(2026, 5, 5),   # Liberation Day  
        date(2026, 5, 14),  # Ascension  
        # ... etc  
    }  
)

# What's the last working day of April 2026?  
# April 30th is Thursday, but April 27th is King's Day  
april_lwd = nl_calendar.last_working_day_of_month(2026, 4)  
# Result: April 30th (Thursday) - King's Day doesn't affect month end

The Readiness Check Pattern

Bring it all together with a readiness check:

@dataclass  
class SourceReadinessResult:  
    """Result of checking if source is ready."""  
    source_entity_id: str  
    snapshot_date: date  
    reference_date: date

status: str  # NOT_EXPECTED, EXPECTED, COMPLETE, STALE  
    has_data: bool  
    data_age_hours: float | None = None  
    blocks_processing: bool = True  
    message: str | None = None

def check_source_readiness(  
    source_config: SourceConfig,  
    snapshot_date: date,  
    calendar: BusinessCalendar,  
) -> SourceReadinessResult:  
    """Check if source is ready for snapshot processing."""  
    # 1. Calculate reference date  
    if source_config.reference_type == "last_working_day":  
        ref_date = calendar.last_working_day(snapshot_date)  
    elif source_config.reference_type == "month_end_working":  
        ref_date = calendar.last_working_day_of_month(  
            snapshot_date.year, snapshot_date.month  
        )  
    else:  
        ref_date = snapshot_date - timedelta(source_config.offset_days)  
    # 2. Check if we expect data for this date  
    if source_config.delivery_schedule == "weekdays":  
        if not calendar.is_working_day(ref_date):  
            return SourceReadinessResult(  
                source_entity_id=source_config.entity_id,  
                snapshot_date=snapshot_date,  
                reference_date=ref_date,  
                status="NOT_EXPECTED",  
                has_data=False,  
                blocks_processing=False,  
                message=f"No delivery expected for {ref_date} (weekend/holiday)"  
            )  
    # 3. Check data presence and freshness  
    data_info = query_source_metadata(source_config.entity_id, ref_date)  
    if not data_info.has_data:  
        return SourceReadinessResult(  
            source_entity_id=source_config.entity_id,  
            snapshot_date=snapshot_date,  
            reference_date=ref_date,  
            status="EXPECTED",  
            has_data=False,  
            blocks_processing=source_config.dependency_class == "mandatory"  
        )  
    # 4. Check staleness  
    age_hours = (datetime.utcnow() - data_info.loaded_at).total_seconds() / 3600  
    is_stale = age_hours > source_config.max_age_hours  
    return SourceReadinessResult(  
        source_entity_id=source_config.entity_id,  
        snapshot_date=snapshot_date,  
        reference_date=ref_date,  
        status="STALE" if is_stale else "COMPLETE",  
        has_data=True,  
        data_age_hours=age_hours,  
        blocks_processing=is_stale and source_config.dependency_class == "mandatory"  
    )

Snapshot Readiness Report

For a complete picture, aggregate all sources:

┌─────────────────────────────────────────────────────────────────┐  
│              EOM Snapshot Readiness: March 2026                 │  
│              Snapshot Date: 2026-03-31                          │  
│              Checked: 2026-03-31 05:30 UTC                      │  
├─────────────────────────────────────────────────────────────────┤  
│  Overall Status: BLOCKED                                        │  
│  Ready to Process: NO                                           │  
├─────────────────────────────────────────────────────────────────┤  
│  MANDATORY SOURCES (2/3 ready)                                  │  
│  ┌──────────────────────────────────────────────────────────┐   │  
│  │ [COMPLETE] GL_TRANSACTIONS                               │   │  
│  │            Ref: 2026-03-31 | Age: 2.5h | Rows: 45,231    │   │  
│  │                                                          │   │  
│  │ [COMPLETE] FX_RATES                                      │   │  
│  │            Ref: 2026-03-31 | Age: 4.1h | Rows: 892       │   │  
│  │                                                          │   │  
│  │ [EXPECTED] POSITIONS  ◄── BLOCKING                       │   │  
│  │            Ref: 2026-03-30 | Waiting for T+1 delivery    │   │  
│  │            Expected by: 2026-03-31 08:00 UTC             │   │  
│  └──────────────────────────────────────────────────────────┘   │  
├─────────────────────────────────────────────────────────────────┤  
│  OPTIONAL SOURCES (1/1 ready)                                   │  
│  ┌──────────────────────────────────────────────────────────┐   │  
│  │ [COMPLETE] MARKET_DATA                                   │   │  
│  │            Ref: 2026-03-31 | Age: 1.2h | Rows: 12,450    │   │  
│  └──────────────────────────────────────────────────────────┘   │  
├─────────────────────────────────────────────────────────────────┤  
│  ENRICHMENT SOURCES (1/1 available)                             │  
│  ┌──────────────────────────────────────────────────────────┐   │  
│  │ [STALE]    COUNTERPARTY_RATINGS                          │   │  
│  │            Ref: 2026-03-24 | Age: 168h | Rows: 5,420     │   │  
│  │            (Within tolerance - weekly update)            │   │  
│  └──────────────────────────────────────────────────────────┘   │  
├─────────────────────────────────────────────────────────────────┤  
│  Next Check: 2026-03-31 05:45 UTC (15 min interval)             │  
│  Estimated Ready: 2026-03-31 08:15 UTC                          │  
└─────────────────────────────────────────────────────────────────┘

Trigger Integration

Connect readiness checks to your orchestration:

class SourceReadinessTrigger:  
    """Trigger that fires when sources are ready."""

def create_trigger(  
        self,  
        pipeline_id: str,  
        source_ids: list[str],  
        snapshot_type: str = "EOM",  
        all_mandatory_required: bool = True,  
        poll_interval_minutes: int = 15,  
        max_wait_hours: int = 24,  
    ) -> str:  
        """Create source readiness trigger."""  
        return self.trigger_manager.create_custom_trigger(  
            name=f"{pipeline_id}_source_readiness",  
            pipeline_id=pipeline_id,  
            config={  
                "type": "source_readiness",  
                "sources": source_ids,  
                "snapshot_type": snapshot_type,  
                "all_mandatory": all_mandatory_required,  
                "poll_interval": poll_interval_minutes,  
                "max_wait": max_wait_hours,  
            }  
        )  
    def evaluate_trigger(  
        self,  
        trigger_id: str,  
        snapshot_date: date,  
    ) -> bool:  
        """Check if trigger should fire."""  
        config = self.get_trigger_config(trigger_id)  
        report = self.delivery_manager.check_snapshot_readiness(  
            snapshot_date=snapshot_date,  
            source_ids=config["sources"],  
        )  
        if config["all_mandatory"]:  
            return report.ready_to_process  
        else:  
            # Fire if any mandatory source is ready  
            return report.mandatory_ready > 0

YAML Configuration

Define source delivery properties in your metadata:

# sources/finance/gl_transactions.yaml  
source:  
  entity_id: ENT_gl_transactions  
  name: General Ledger Transactions

delivery:  
    schedule: daily  
    reference_date_type: calendar_day  
    reference_date_offset: 0  # T+0  
  dependency:  
    classification: mandatory  
    max_age_hours: 24  
    staleness_warning_hours: 12  
  calendar:  
    calendar_id: NL  
    skip_holidays: false

# sources/finance/positions.yaml  
source:  
  entity_id: ENT_positions  
  name: Trading Positions  
  delivery:  
    schedule: weekdays  
    delivery_time_utc: "08:00"  
    reference_date_type: calendar_day  
    reference_date_offset: 1  # T+1  
  dependency:  
    classification: mandatory  
    max_age_hours: 36  # Allow for weekend delays  
  calendar:  
    calendar_id: NL  
    skip_holidays: true

# sources/market/market_data.yaml  
source:  
  entity_id: ENT_market_data  
  name: Market Data Feed  
  delivery:  
    schedule: daily  
    reference_date_type: last_working_day  
  dependency:  
    classification: optional  
    max_age_hours: 48  
    staleness_warning_hours: 12  
    fallback_source_id: ENT_market_data_backup

Pipeline Integration

Wire it into your pipeline definition:

# pipelines/eom_financial_snapshot.yaml  
pipeline:  
  name: eom_financial_snapshot  
  description: End-of-month financial data consolidation

trigger:  
    type: source_readiness  
    snapshot_type: EOM  
    all_mandatory_required: true  
    poll_interval_minutes: 15  
    max_wait_hours: 48  
  sources:  
    mandatory:  
      - ENT_gl_transactions  
      - ENT_fx_rates  
      - ENT_positions  
    optional:  
      - ENT_market_data  
    enrichment:  
      - ENT_counterparty_ratings  
  on_ready:  
    # When all mandatory sources are ready  
    start_pipeline: true  
    notify:  
      - channel: "#data-platform"  
        message: "EOM snapshot starting - all sources ready"  
  on_timeout:  
    # If max_wait_hours exceeded  
    action: notify_and_wait  
    notify:  
      - channel: "#data-platform"  
        message: "EOM snapshot blocked - waiting for: {blocking_sources}"

Key Patterns

1. Weekend Handling

For weekday-only sources, don’t expect data on weekends:

if delivery_schedule == "weekdays":  
    if snapshot_date.weekday() >= 5:  # Sat/Sun  
        # Reference date is Friday  
        reference_date = snapshot_date - timedelta(  
            days=snapshot_date.weekday() - 4  
        )

2. Month-End Working Day

For EOM snapshots that need working day data:

def last_working_day_of_month(year: int, month: int) -> date:  
    # Get last calendar day  
    if month == 12:  
        last = date(year + 1, 1, 1) - timedelta(days=1)  
    else:  
        last = date(year, month + 1, 1) - timedelta(days=1)

# Walk back to working day  
    while last.weekday() >= 5 or last in holidays:  
        last = last - timedelta(days=1)  
    return last

3. Fallback Sources

When primary source fails, use backup:

if primary_result.status == "FAILED":  
    if config.fallback_source_id:  
        fallback_result = check_source_readiness(  
            config.fallback_source_id,  
            snapshot_date  
        )  
        if fallback_result.has_data:  
            return fallback_result.with_warning(  
                "Using fallback source"  
            )

Benefits

  1. Accurate Timing : Start processing exactly when data is ready, not before
  2. Fewer Failures : Don’t attempt processing with incomplete data
  3. Clear Dependencies : Document what’s mandatory vs optional
  4. Audit Trail : Record which sources were available at snapshot time
  5. Intelligent Retries : Know when to check again vs when to alert

Conclusion

Source delivery properties bridge the gap between “pipeline dependency management” and “data readiness.” By modeling:

  • Delivery schedules (daily, weekdays, weekly)
  • Reference date logic (T+0, T+1, last working day)
  • Dependency classes (mandatory, optional, enrichment)
  • Business calendars (holidays, working days)

…you can answer the question every data team asks: “When is my data actually ready?”

The integration layer becomes smarter. It knows that Friday’s weekend source data won’t arrive until Monday. It understands that EOM snapshots need the last working day, not calendar month-end. It starts processing precisely when conditions are met.

That’s the power of metadata-driven orchestration.


Next in this series: “Snapshot Period Management: Beyond Simple Date Ranges”