30 Nov 2025

DET week 1 homework 1: redlining compile-time data contracts for clarity wins

Workshop context

This is week 1 homework for the DET (Data Engineer Things) Technical writing workshop led by Yaakov Bressler .

Workshop site: dataengineerthings.org

Workshop mental model

The DET workshop uses a 7 Writer Objectives × 6 Reader Objectives matrix to align writer intent with reader needs.

%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px'}}}%%
flowchart TD
    W["Writer Objectives(7)"] --> M["Objectives Matrix 7×6 = 42 alignments"]
    R["Reader Objectives(6)"] --> M
    M --> A["Alignment Find best match"]
    A --> T["Transform Adjust content"]

    W --> EX["Examples:• Teach | Resource | Convince"]
    R --> EX2["Examples:• Learn | Reference | Decide"]

    style M fill:#e1f5ff,stroke:#0066cc,color:#000
    style A fill:#d4edda,stroke:#28a745,color:#000
    style T fill:#fff3cd,stroke:#ffc107,color:#000

Assignment context

Homework 1: Redline the compile-time data contracts article (original here ) to show transformation from TeachResource objective.

Reviewer instructions

This redlined document uses:

  • Strike-through for content to REMOVE
  • Admonition boxes explaining WHY each decision
  • Plain text for content to KEEP
  • Visual emphasis on the 70% reduction when objectives align

The goal: demonstrate objectives-driven editing through transparent redlining.


Original vs target objectives

Original article objective: Teach

Writer intent: Build understanding from scratch

Reader need: Learn compile-time contracts conceptually

Content structure:

  • Personal stories - “Once, an upstream team silently renamed..”
  • Mental models - “Here’s how to think about macros..”
  • Step-by-step walkthroughs - “Let’s break this down line by line..”
  • Philosophy and motivation - “Why this matters”

Target objective: Resource

Writer intent: Provide quick reference for experts

Reader need: Look up API, patterns, gotchas

Content structure:

  • Prerequisites (what you need to know first)
  • Quick start (runnable code immediately)
  • API reference (shapes, policies, macros)
  • Production patterns (what works in the real world)
  • Gotchas (common mistakes)

Transformation strategy

This redlining systematically removes Teaching content (stories, walkthroughs, philosophy) while preserving Resource content (code, patterns, references, gotchas).

Expected reduction: ~70% (from 1,751 lines to ~500-600 lines of Resource content + homework annotations).


Redlining method

Using Talmudic commentary style:

  1. Circle (keep): Code examples, production patterns, gotchas, references
  2. Cross (remove): Stories, mental models, step-by-step explanations, “What’s happening” sections
  3. Annotate (explain): WHY each decision using admonition boxes

EVERY line from the original is preserved below - either struck-through with explanation or kept with explanation.


Complete original article (redlined)

What if broken pipelines never launched - because the compiler stopped them?

> “If it compiles, contracts align."

❌ REMOVE opening hook

Why remove: The philosophical opening (“What if…”) is Teaching tone. Resource readers already know the problem (it’s in their search query: “Scala compile-time data contracts”). They need the solution immediately, not motivation.

What this served in Teaching version:

  • Built curiosity for learners
  • Positioned problem as worth solving
  • Set philosophical tone

Why it doesn’t fit Resource:

  • Resource objective = utility, not persuasion
  • Experts don’t need to be sold on the approach
  • Opening should be Prerequisites + Quick Start instead

I’ve been working on this problem for a while now. You know how it goes - you’re running a data pipeline, everything’s fine, then someone changes a field name upstream and boom, your job crashes at 2 AM. The on-call engineer (probably you) gets paged, and you spend the next hour figuring out what went wrong.

Once, an upstream team silently renamed a column amountamt. Our pipeline didn’t crash immediately - it happily wrote nulls for weeks. By the time we caught it, we had to backfill millions of records. That’s the day I realized: runtime validation catches problems late; compile-time contracts catch them earlier.

❌ REMOVE personal story (2 AM incident)

Why remove: Resource readers already had their 2 AM moment. That’s why they’re searching for this solution. Personal stories build empathy for learners, not experts.

What this served in Teaching version:

  • Built empathy with reader
  • Explained problem in relatable terms
  • Motivated the solution
  • Established authority (“I’ve faced this”)

Why it doesn’t fit Resource:

  • Experts don’t need motivation, they need the solution
  • They already understand the problem (it’s implicit in their search)
  • Resource objective = utility, not storytelling
  • Takes 150 words to say what experts already know

Word count removed: ~150 words

This post shows you how to catch these issues at compile time. Not at runtime. Not even at test time. At compile time. Your code literally won’t build if schemas drift.

❌ REMOVE promise paragraph

Why remove: The promise (“this post shows you how”) is implicit in a Resource article. Title already states what it is: “Compile-time data contracts in Scala 3.”

What this served in Teaching version:

  • Set expectations for tutorial journey
  • Built anticipation
  • Positioned value proposition

Why it doesn’t fit Resource:

  • Resource readers know what they want from the title
  • Don’t tell them what you’ll teach them - just give them the API
  • “Shows you how” implies tutorial, Resource provides reference

Word count removed: ~35 words

We’ll build this from scratch - first understand how Scala 3 macros work, then write a minimal contract system that proves schema compatibility before your code even runs. I’ll also show the Scala 2 version because, let’s be honest, many teams are still on Scala 2.

❌ REMOVE build plan

Why remove: “We’ll build this from scratch” is the Teaching objective speaking. Resource doesn’t build, it provides finished components.

What this served in Teaching version:

  • Outlined tutorial structure
  • Set learning journey expectations
  • Promised comprehensive coverage
  • Built progressive understanding

Why it doesn’t fit Resource:

  • Resource readers want the answer, not the journey
  • “Build from scratch” implies tutorial
  • Resource = here’s the thing, here’s the code, done
  • Teaching progression (understand → write → prove) doesn’t apply

Word count removed: ~45 words

⭕ KEEP github link (but enhance)

Decision: Keep the github reference but move it to Quick Start section.

Why it fits Resource: Resource readers want to clone and run immediately. Working code repository is pure utility.

Enhancement needed: Make it more prominent in a Quick Start section with actual commands instead of buried in opening paragraphs.

Original text preserved below for reviewer:

All code here runs in a vanilla sbt project. You can grab the working code in github from https://github.com/vim89/compile-time-data-contracts and run it yourself.

## Why this matters

Look, I get it. Writing tests is important. But tests are samples. They check what you think might break. The compiler, though? It’s exhaustive for structure. It can prove your schemas match - every field, every type, every time.

Here’s the thing - schemas change all the time in real projects. And when they do: - Your build should fail early, not your production job - Keep transformations pure and effects at the edges (so retries don’t duplicate writes) - Let the compiler do the heavy lifting

❌ REMOVE 'Why this matters' section

Why remove: Philosophy section. Resource readers already know why it matters (it’s in their search query).

What this served in Teaching version:

  • Motivated learners who might be skeptical
  • Explained value proposition (“tests vs compiler”)
  • Built case for approach
  • Positioned as best practice

Why it doesn’t fit Resource:

  • Resource objective = utility, not persuasion
  • Experts don’t need to be sold
  • “Why” is for Teaching, “How” is for Resource
  • Takes 110 words to explain what’s implicit in the title

Word count removed: ~110 words

Important upfront: This focuses on preventing schema drift within your controlled boundaries - the systems you build, deploy, and version. It doesn’t replace runtime validation for external data sources or third-party APIs. Think of it as catching a large class of schema mismatches during development and deployment, not eliminating all drift everywhere. You’ll still need runtime checks for data coming from systems you don’t control.

Scope boundary
Compile-time contracts catch drift in your controlled codebase - the transforms you write, the case classes you define, the schemas you version together. They won’t catch upstream API changes, late-arriving schema registry updates, or external data sources that change independently. Use compile-time for what you control, runtime validation for everything else. Defense in depth, not either/or.
⭕ KEEP scope boundary warning

Decision: Keep both the paragraph and the admonition.

Why it fits Resource:

  • Honest about limitations (what it CAN’T do)
  • Sets realistic expectations
  • Resource readers appreciate knowing boundaries
  • Prevents misuse (trying to use it for external APIs)
  • Saves reader time (don’t try to use it for X)

Why this is Resource, not Teaching:

  • Not selling the approach, warning about boundaries
  • Practical, pragmatic
  • Gotcha/caveat content fits Resource objective

Word count kept: ~100 words

## What we’ll build

We’re going to build a small compile-time validation system: 1. A way to describe case class structure at compile time (we’ll call it “shape”) 2. A policy system - Exact, Backward, Forward compatibility modes 3. A macro that proves “these two shapes conform under policy P” - or refuses to compile 4. A phantom-typed pipeline builder that enforces correct construction order

First, Scala 3 (using inline + quotes), then Scala 2 (using blackbox macros). I’ll point out the differences as we go.

❌ REMOVE 'What we'll build' section

Why remove: Tutorial roadmap. Resource readers don’t need a learning journey, they need the API reference immediately.

What this served in Teaching version:

  • Set expectations for progressive learning
  • Outlined tutorial structure
  • Built anticipation for upcoming sections
  • Promised comprehensive coverage

Why it doesn’t fit Resource:

  • “We’re going to build” is Teaching language
  • Resource doesn’t build, it provides finished components
  • Table of contents serves this function better
  • Numbered list implies sequential learning path

Word count removed: ~80 words

### Run the code in github

❌ REMOVED: Github clone commands (click to see what was removed)
1
2
3
4
5
git clone https://github.com/vim89/compile-time-data-contracts.git
cd compile-time-data-contracts

# Scala 3
sbt "runMain ctdc.CtdcPoc"
❌ REMOVED: Info admonition about compile-fail examples (click to see what was removed)
The code in github includes working examples you can run immediately. All compile-fail cases are documented inline - uncomment them to see the compiler errors.
⭕ KEEP github quick start (but move)

Decision: Keep this content but move it earlier to a dedicated Quick Start section.

Why it fits Resource:

  • Immediate value (30 seconds to running code)
  • No preamble, just commands
  • Working examples = pure utility

Transformation needed:

  • Move to top of article (after Prerequisites)
  • Add Prerequisites section first
  • Expand with basic usage example (success + failure case)

Original text preserved here for reviewer, will be reorganized in Resource version:


## Part 1 - Scala 3: the mental model (inline + quotes)

Scala 3 macros are different from Scala 2. Better, actually. They’re safer and easier to reason about.

Two main ideas: - inline def: tells the compiler “expand this at compile time” - quotes + Expr[T]: gives you a typed AST that you can inspect

❌ REMOVED: Macro execution sequence diagram (click to see what was removed)
sequenceDiagram
    actor Dev
    participant Compiler
    participant Macro as "Conforms.impl"
    participant Reflect as "TypeRepr/quotes"
    Dev->>Compiler: compile
    Compiler->>Macro: splice ${ ... }
    Macro->>Reflect: inspect types
    Reflect-->>Macro: shapes for Out and Contract
    alt conforming
        Macro-->>Compiler: emit Conforms[Out,Contract,P]
        Compiler-->>Dev: build succeeds
    else drift
        Macro-->>Compiler: report.errorAndAbort(msg)
        Compiler-->>Dev: compile error with drift details
    end

Here’s a minimal macro to see how it works:

❌ REMOVED: IntroMacro example code (click to see what was removed)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// build.sbt: use Scala 3
// scalaVersion := "3.3.3"

// src/main/scala/com/example/IntroMacro.scala
package com.example
import scala.quoted.*

object IntroMacro:
  inline def show(inline x: Any): String = ${ showImpl('x) }
  private def showImpl(x: Expr[Any])(using Quotes): Expr[String] =
    Expr(s"You passed: ${x.show}")

What’s happening here? - The inline method is what users call - The ${ ... } splice calls the implementation - The implementation gets 'x (quoted code) as Expr[Any] - We return Expr[String] - the AST for what the compiler will embed

Try it:

❌ REMOVED: IntroMacro demo usage (click to see what was removed)
1
2
3
4
// src/main/scala/com/example/Demo.scala
package com.example
@main def run(): Unit =
  println(IntroMacro.show(1 + 2)) // prints: You passed: 1.+(2)

See how it printed the AST of 1 + 2? That’s the power here - we can inspect code structure at compile time.

Check out the Scala 3 macros overview and best practices for more details.

❌ REMOVED: Tip admonition for macro debugging (click to see what was removed)
If you’re new to Scala 3 macros, start by reading macro-generated code in the REPL: sbt> console, then scala> import scala.quoted.*, then inspect what your macros actually expand to. Understanding the generated code is 80% of debugging macro issues. The compiler’s doing the work - you just need to see what it’s actually producing.
❌ REMOVE entire Part 1 mental model

Why remove: Complete Teaching section. “Mental model” is tutorial content for learners, not reference for experts.

What this served in Teaching version (~800 words):

  • Introduced inline def concept from scratch
  • Explained quotes and Expr[T] with examples
  • Built understanding with minimal IntroMacro.show example
  • “What’s happening here?” walkthrough (Teaching language)
  • “Try it” exercise (Teaching technique)
  • “See how it printed…?” rhetorical question (Teaching engagement)
  • Sequence diagram showing macro execution flow
  • Tip admonition for debugging (valuable but assumes learning)

Why it doesn’t fit Resource:

  • Resource readers already know Scala 3 macros (stated in Prerequisites)
  • “Mental model” is for building understanding, Resource provides reference
  • Teaching progression (introduce → explain → demonstrate → exercise)
  • If readers don’t know macros, Prerequisites already told them to leave
  • External links to Scala docs handle this better

Alternative for Resource version:

  • Prerequisites section states: “Understanding of inline, quotes, Expr[T], TypeRepr
  • Links to official Scala 3 docs for those who need it
  • Jump straight to Shape API

Word count removed: ~800 words


Part 2 - Describe shapes at compile time (Scala 3)

Now we need to describe case class structure. In real projects, you might use Shapeless, Magnolia, or Scala 3’s Mirror. I’ll show a compact Mirror-based version here because it’s self-contained.

The idea: a typeclass Shape[A] that knows the fields of A.

❌ REMOVE walkthrough introduction

Why remove: “Now we need to…” and “I’ll show…” is Teaching language. Resource version jumps straight to the API code.

What this served in Teaching version:

  • Positioned as next step in learning journey
  • Explained why this section matters
  • Set context for code to follow

Why it doesn’t fit Resource:

  • Resource readers scan for Shape API, don’t need narrative
  • “We need to” implies tutorial progression
  • Just show the API with minimal comment

Alternative for Resource: “Shape API (Mirror-based)” as section heading, code immediately follows.

Word count removed: ~40 words

classDiagram
    class Field {
      +name: String
      +tpe: String
      +hasDefault: Boolean
      +isOptional: Boolean
    }
    class Shape {
      +fields(): List~Field~
    }
    Shape "1" --> "*" Field : returns
⭕ KEEP class diagram

Decision: Keep the class diagram.

Why it fits Resource:

  • Visual API reference showing Field and Shape structure
  • Quick lookup (“what fields does Field have?”) without reading code
  • Resource readers appreciate visual summaries they can scan
  • If not needed, easily skipped

Why this is Resource, not Teaching:

  • Not explaining concepts, just showing structure
  • In playbook terms: “visual reference” not “teaching aid”
  • Comparable to API documentation diagrams

Word count kept: (diagram)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/main/scala/com/example/Shape.scala
package com.example

import scala.deriving.Mirror

final case class Field(name: String, tpe: String, hasDefault: Boolean = false, isOptional: Boolean = false)
trait Shape[A]:
  def fields: List[Field]

object Shape:
  given Shape[String] with { def fields = Nil }
  given Shape[Int]     with { def fields = Nil }
  given Shape[Long]    with { def fields = Nil }
  given [A]: Shape[Option[A]] with { def fields = Nil } // element optionality handled elsewhere

  inline given derived[A](using m: Mirror.Of[A]): Shape[A] =
    inline m match
      case p: Mirror.ProductOf[A] =>
        val labels = constValueTuple[p.MirroredElemLabels]
        val types  = typeNames[p.MirroredElemTypes]
        val zipped = zip(labels, types)
        new Shape[A] {
          def fields: List[Field] =
            zipped.map { (name, tpe) => Field(name, tpe, hasDefault = false, isOptional = tpe.startsWith("Option[")) }.toList
        }
      case _ => new Shape[A] { def fields = Nil }

  import scala.compiletime.{erasedValue, constValueTuple}
  private inline def typeNames[T <: Tuple]: List[String] = inline erasedValue[T] match
    case _: EmptyTuple => Nil
    case _: (h *: t)   => summonTypeName[h] :: typeNames[t]

  private inline def summonTypeName[T]: String = constValue["" + T]

  private inline def zip[L <: Tuple, R <: List[String]](labels: L, types: List[String]): List[(String, String)] =
    inline labels match
      case _: EmptyTuple => Nil
      case _: (h *: t)   => constValue[h].asInstanceOf[String] -> types.head :: zip[t, List[String]](erasedValue, types.tail)
⭕ KEEP Shape implementation code

Decision: Keep the complete implementation.

Why it fits Resource:

  • This IS the API - Field case class, Shape trait, derivation logic
  • Resource readers need the actual code to use or adapt
  • Implementation details matter for experts (Mirror.Of, constValueTuple, etc.)
  • Copy-paste ready for production use

Why this is Resource, not Teaching:

  • No explanation of how it works (just the code)
  • Comments are minimal, API-level
  • Expert readers can read the implementation
  • Teaching version would explain Mirror, constValueTuple, inline match, etc.

Word count kept: ~150 words (code)

Yeah, it’s a bit dense. But here’s what it does: - Uses Scala 3’s Mirror to reflect on case classes at compile time - Extracts field names and types - Marks optional fields (those wrapped in Option) - Returns a List[Field] describing the structure

❌ REMOVE code walkthrough

Why remove: “Here’s what it does” is Teaching language. Resource readers read the code directly.

What this served in Teaching version:

  • Explained dense code for learners
  • Bullet-point summary of functionality
  • Made code approachable

Why it doesn’t fit Resource:

  • Experts read implementation, don’t need summary
  • “Yeah, it’s a bit dense” apologizes to learners
  • Resource assumes you can read Scala 3 metaprogramming
  • Inline comments in code serve this better

Word count removed: ~60 words

❌ REMOVED: Shape derivation flow diagram (click to see what was removed)
flowchart LR
    A["Case class A(...)"] --> M["Mirror.Of[A]"]
    M --> L["MirroredElemLabels"]
    M --> T["MirroredElemTypes"]
    L --> Z["zip(labels, types)"]
    T --> Z
    Z --> S["Shape[A].fields: List[Field]"]
❌ REMOVE flow diagram

Why remove: Teaching aid showing “how it works” step-by-step. Resource readers understand Mirror already (or they shouldn’t be here per Prerequisites).

What this served in Teaching version:

  • Visual walkthrough of derivation process
  • Showed data flow through Mirror API
  • Helped learners understand execution

Why it doesn’t fit Resource:

  • Explains the process, Resource just gives the tool
  • If you need this diagram, you’re missing prerequisites
  • Code itself shows the flow for experts

Word count removed: (diagram)

Usage:

1
2
3
4
5
6
7
8
// src/test/scala/com/example/ShapeSpec.scala
package com.example

final case class User(id: Long, email: String, note: Option[String])

@main def checkShape(): Unit =
  val s = summon[Shape[User]]
  println(s.fields) // List(Field("id","Long"), Field("email","String"), Field("note","Option[String]",false,true))
⭕ KEEP usage example

Decision: Keep the usage code.

Why it fits Resource:

  • Shows working example of Shape API
  • Demonstrates summon syntax
  • Expected output in comment is helpful reference
  • Copy-paste ready

Why this is Resource, not Teaching:

  • No “Try it” language
  • No explanation, just working code
  • Shows input → output clearly

Word count kept: ~30 words (code)

Now we can ask the compiler “what fields does this case class have?” at compile time. That’s the foundation.

❌ REMOVE summary sentence

Why remove: “Now we can…” and “That’s the foundation” are Teaching narrative. Resource readers know what the API does from reading it.

What this served in Teaching version:

  • Reinforced what was learned
  • Positioned as building block for next section
  • Motivational language

Why it doesn’t fit Resource:

  • Resource doesn’t narrate what you learned
  • “Foundation” implies progressive building (Teaching)

Word count removed: ~25 words

If you prefer libraries, check out Magnolia - it works great for both Scala 2 and 3.

⭕ KEEP Magnolia reference

Decision: Keep the library alternative.

Why it fits Resource:

  • Practical alternative for production use
  • Resource readers appreciate knowing options
  • External link is pure utility

Why this is Resource, not Teaching:

  • Not explaining Magnolia, just referencing it
  • Gives expert readers choice
  • Production-focused (mentions Scala 2 + 3 compat)

Word count kept: ~20 words


Part 3 - Policies: Exact / Backward / Forward

Here’s where we define compatibility rules. Think of these as your migration strategies:

❌ REMOVE introductory narrative

Why remove: “Here’s where we define…” is Teaching setup. Resource readers scan for the API directly.

What this served in Teaching version:

  • Positioned section in tutorial flow
  • Set context for learners

Why it doesn’t fit Resource:

  • Section heading already describes content
  • Resource jumps straight to code

Word count removed: ~15 words

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/main/scala/com/example/Policy.scala
package com.example

sealed trait Policy
object Policy:
  sealed trait Exact     extends Policy
  sealed trait Backward  extends Policy // producer can add optional/defaulted fields
  sealed trait Forward   extends Policy // consumer tolerates extra fields
  case object Exact    extends Exact
  case object Backward extends Backward
  case object Forward  extends Forward
⭕ KEEP Policy trait code

Decision: Keep the complete Policy trait implementation.

Why it fits Resource:

  • This IS the API - core type definitions
  • Three policy types (Exact, Backward, Forward) are the API surface
  • Inline comments explain each policy succinctly
  • Copy-paste ready for production use

Why this is Resource, not Teaching:

  • No walkthrough, just code
  • Comments are API-level documentation
  • Expert readers understand sealed trait patterns

Word count kept: ~50 words (code)

Why three policies?

  • Exact: Schemas must match exactly. No surprises. Use this for critical paths.
  • Backward: Producer can add optional fields. Useful when rolling out new features without breaking consumers.
  • Forward: Consumer ignores extra fields. Useful when the producer might send more than you need.
⭕ KEEP policy explanations

Decision: Keep the three-bullet explanation.

Why it fits Resource:

  • Concise API documentation (what each policy does)
  • Use case guidance (when to use each)
  • Not tutorial explanation, just reference
  • Answers “which policy should I use?” quickly

Why this is Resource, not Teaching:

  • Direct, practical descriptions
  • Focused on production usage (“critical paths”, “rolling out”)
  • No conceptual building, just what it does

Word count kept: ~60 words

flowchart LR
    EX["Policy.Exact"]
    BW["Policy.Backward"]
    FW["Policy.Forward"]

    EX --- BW
    EX --- FW

    EX:::tight -->|no diffs allowed| OK1["✔ match only"]
    BW:::relax -->|allow missing optional in producer\nallow extra fields in producer| OK2["✔ migration adds"]
    FW:::relax -->|allow extra fields in consumer side| OK3["✔ tolerant reader"]

    classDef tight fill:#eef,stroke:#446;
    classDef relax fill:#efe,stroke:#484;
⭕ KEEP policy relationships diagram

Decision: Keep the flowchart showing policy relationships.

Why it fits Resource:

  • Visual reference showing differences between policies
  • Quick lookup for which policy allows what
  • Not explaining concepts, showing constraints visually

Why this is Resource, not Teaching:

  • Comparable to API documentation diagrams
  • Shows rules/constraints at a glance
  • Practical reference, not educational walkthrough

Word count kept: (diagram)

Visual (save/share):

Policy lattice

A tiny policy lattice - Exact / Backward / Forward.

⭕ KEEP policy lattice figure

Decision: Keep the SVG figure reference (but remove “Visual (save/share)” preamble).

Why it fits Resource:

  • Alternative visual representation
  • Resource readers appreciate multiple formats
  • Figure is reference material, not teaching aid

Why remove preamble:

  • “Visual (save/share):” is unnecessary narration
  • Figure speaks for itself

Word count kept: (figure), removed: ~3 words preamble

In practice: start with Exact, relax to Backward/Forward during migrations, then tighten back to Exact once stable.

Migration strategy
The policy progression that works in production: Exact for stable pipelines, Backward during producer rollouts (allows new optional fields), Forward during consumer updates (tolerates extras), then back to Exact after migration completes. Document the expected timeline - “Backward policy through Q2 migration, then tightening to Exact.” Prevents policy drift from becoming permanent.
⭕ KEEP migration guidance

Decision: Keep both the paragraph and the admonition.

Why it fits Resource:

  • Production best practices (how to actually use these policies)
  • Not teaching the concept, sharing real-world strategy
  • Practical timeline advice (“document expected timeline”)
  • Gotcha prevention (“Prevents policy drift from becoming permanent”)

Why this is Resource, not Teaching:

  • “In practice” = lessons from production
  • Specific, actionable guidance
  • Resource readers need migration strategies

Word count kept: ~80 words


## Part 4 - How this fits with existing tools

Before we dive deeper, you might be thinking: “We already have Avro, Protobuf, schema registries, and Great Expectations. Why add compile-time contracts?"

Fair question. Here’s how they compare:

Schema Registry (Confluent, AWS Glue) - What it does: Centralized schema storage with version control - When it helps: Runtime validation across services, schema evolution tracking - The gap: You find mismatches when the job runs, not when you compile - Use together: Schema registry for cross-service contracts; compile-time for intra-repo checks

Avro / Protobuf - What they do: Binary serialization with embedded schemas - When they help: Network efficiency, strong contracts between services - The gap: Schema compatibility is checked at runtime or via external tooling - Use together: Avro/Protobuf for wire format; compile-time checks to ensure your case classes match those schemas

Great Expectations / Deequ - What they do: Runtime data quality validation (nulls, ranges, distributions) - When they help: Catching bad data values, statistical anomalies - The gap: They validate data content, not schema structure at compile time - Use together: Compile-time for structure; Great Expectations for data quality

The compile-time advantage: Catches drift in your controlled codebase before deployment. If your transform expects field X but the source produces field Y, your build fails - not your production job. Think of it as an additional safety layer for systems you build and version together.

Real scenario: You have Avro schemas in a registry, but your Scala pipeline reads them into case classes. Compile-time contracts verify those case classes match the expected contract before you deploy. If upstream changes the Avro schema, your build breaks - you don’t wait for the job to crash.

❌ REMOVE entire Part 4 tool comparison

Why remove: Teaching content explaining “why this approach” by comparing alternatives. Resource readers already decided to use this tool (it’s in their search query).

What this served in Teaching version (~350 words):

  • Addressed skepticism (“We already have Avro…”)
  • Positioned against existing tools (Schema Registry, Avro, Protobuf, Great Expectations)
  • Explained gaps in each alternative
  • Built case for adopting this approach
  • “Fair question” acknowledges reader doubts (Teaching empathy)

Why it doesn’t fit Resource:

  • Resource objective = utility, not persuasion
  • Experts don’t need to be sold on the approach
  • “Before we dive deeper, you might be thinking…” is Teaching narration
  • Comparison matrices are for deciding, not using
  • If reader needs this, they’re at wrong article (Prerequisites failed)

Alternative for Resource:

  • Brief Integration section showing “Use with Avro” code example
  • Not explaining why, just showing how to combine

Word count removed: ~350 words


## Part 5 - Understanding TypeRepr: The compiler’s view of types

Before we dive into the macro, you need to understand TypeRepr - Scala 3’s way of representing types during compilation.

Think of it like this: When you write List[String], the compiler doesn’t just see text. It sees a structured tree:

❌ REMOVED: ASCII tree diagram of TypeRepr (click to see what was removed)
1
2
3
4
AppliedType(List, [String])
   │               │
   │               └─ TypeRepr for String
   └─ Type constructor
❌ REMOVED: TypeRepr structure flowchart (click to see what was removed)
flowchart TB
    A["List[String]"] --> B["AppliedType(List,[String])"]
    B --> C["tycon = List"]
    B --> D["args = [String]"]

Key TypeRepr operations:

❌ REMOVED: TypeRepr operations code examples (click to see what was removed)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val t: TypeRepr = TypeRepr.of[List[String]]

// 1. Subtype checking: Is List[String] a subtype of Seq[?]?
t <:< TypeRepr.of[Seq[?]]  // true

// 2. Type equality: Is this exactly String?
t =:= TypeRepr.of[String]  // false

// 3. Pattern matching: Break it apart
t match
  case AppliedType(tycon, args) =>
    // tycon = List
    // args = [String]

Why we need this: To compare schemas, we need to ask questions like: - “Is this field an Option?" - “What’s inside this List?" - “Does this case class have a primary constructor?"

TypeRepr lets us answer these at compile time.

❌ REMOVE entire Part 5 TypeRepr tutorial

Why remove: Complete Teaching section. “Understanding TypeRepr” teaches macro fundamentals from scratch.

What this served in Teaching version (~200 words):

  • Explained TypeRepr concept for learners
  • “Think of it like this” (Teaching metaphor)
  • Visual tree diagram showing structure
  • Code examples demonstrating operations
  • “Why we need this” motivation paragraph
  • “Before we dive into the macro, you need to understand” (Teaching prerequisite setup)

Why it doesn’t fit Resource:

  • Resource readers already know TypeRepr (stated in Prerequisites: “Understanding of TypeRepr”)
  • Teaching progression (explain concept → show examples → motivate usage)
  • If readers don’t understand TypeRepr, Prerequisites already told them to leave
  • External links to Scala 3 docs handle this better

Alternative for Resource version:

  • Prerequisites states: “Understanding of TypeRepr”
  • Links to official Scala 3 macro docs
  • Jump straight to implementation code

Word count removed: ~200 words


## Part 5 - The TypeInspector pattern: One question at a time

Now let’s build the utilities. Each function answers ONE question about a type. Start simple:

❌ REMOVE entire Part 5 TypeInspector tutorial walkthrough

Why remove: Complete Teaching section with step-by-step walkthrough. “Each function answers ONE question” is tutorial narrative.

What this served in Teaching version (~650 words):

  • “Now let’s build…” (Teaching progression)
  • 6 inspector functions with detailed walkthroughs
  • “What’s happening:” explanations for each (Teaching technique)
  • “Try it mentally:” exercises (Teaching engagement)
  • “Examples:” with commented outputs (Teaching demonstration)
  • “Why we need this:” motivation paragraphs
  • “The pattern recap:” summary (Teaching reinforcement)

Why it doesn’t fit Resource:

  • Resource readers need the implementation code, not the walkthrough
  • “What’s happening” explains code line-by-line (Teaching)
  • “Try it mentally” is a learning exercise
  • If readers can’t understand TypeRepr code, Prerequisites failed
  • Inspector implementations are straightforward for experts

What to KEEP: None of the walkthrough text. If these functions are useful utilities, show them as a code block without explanation. But given they’re internal helpers for the main macro, they may not need to be in Resource version at all.

Word count removed: ~650 words

❌ REMOVED: All 6 TypeInspector functions with walkthroughs (~650 words - click to see what was removed)

Inspector 1: Is this a case class?

1
2
3
def isCaseClass(t: TypeRepr): Boolean =
  val s = t.typeSymbol  // Get the symbol (the "name" of the type)
  s.isClassDef && s.flags.is(Flags.Case)

What’s happening:

  • typeSymbol - Every type has a symbol (like a unique ID)
  • isClassDef - Is it a class? (not a trait, object, etc.)
  • flags.is(Flags.Case) - Does it have the case modifier?

Try it mentally:

1
2
3
case class User(id: Long)  // ✅ isClassDef=true, has Case flag
trait UserTrait            // ❌ isClassDef=false
object UserObject          //  not a class

Inspector 2: What are the type arguments?

1
2
3
def appliedArgs(t: TypeRepr): List[TypeRepr] = t match
  case AppliedType(_, args) => args  // Extract the arguments
  case _                    => Nil   // No arguments

What’s happening:

  • AppliedType - Pattern for generic types like List[String] or Map[String, Int]
  • args - The type parameters: [String] or [String, Int]

Examples:

1
2
3
4
5
6
7
8
List[String]        // AppliedType(List, [String])
                    // appliedArgs returns [String]

Map[String, Int]    // AppliedType(Map, [String, Int])
                    // appliedArgs returns [String, Int]

String              // Not an AppliedType
                    // appliedArgs returns Nil

Inspector 3: Is this an Option?

1
2
3
4
def optionArg(t: TypeRepr): Option[TypeRepr] =
  if t <:< TypeRepr.of[Option[?]] then  // Is it a subtype of Option[Something]?
    appliedArgs(t).headOption           // Yes! Extract the Something
  else None                             // Nope

What’s happening:

  • <:< - Subtype check: “Is this type compatible with Option[?]?”
  • Option[?] - The ? means “Option of anything”
  • appliedArgs(t).headOption - Get the first (and only) type argument

Examples:

1
2
3
4
5
6
7
8
9
Option[String]  // ✅ Is subtype of Option[?]
                // appliedArgs returns [String]
                // headOption returns Some(String)

Some[Int]       // ✅ Some is subtype of Option
                // Returns Some(Int)

String          // ❌ Not an Option
                // Returns None

Why we need this: Field-level optionality. When we see age: Option[Int], we need to know:

  1. The field itself is optional
  2. The underlying type is Int

Inspector 4: Is this a sequence?

1
2
3
4
5
6
7
8
def seqArg(t: TypeRepr): Option[TypeRepr] =
  val isSeqLike =
    t <:< TypeRepr.of[List[?]] ||
    t <:< TypeRepr.of[Seq[?]] ||
    t <:< TypeRepr.of[Vector[?]] ||
    t <:< TypeRepr.of[Array[?]] ||
    t <:< TypeRepr.of[Set[?]]
  if isSeqLike then appliedArgs(t).headOption else None

What’s happening:

  • Check if it’s ANY kind of sequence type
  • If yes, extract what’s inside (the element type)

Why check multiple types? Because Scala has many collection types:

1
2
3
4
5
List[String]    // ✅ Is List[?]
Seq[Int]        // ✅ Is Seq[?]
Vector[Long]    // ✅ Is Vector[?]
Array[Byte]     // ✅ Is Array[?]
Set[String]     //  Is Set[?]

All these should become SequenceShape(element).

Inspector 5: Is this a Map?

1
2
3
4
5
6
def mapArgs(t: TypeRepr): Option[(TypeRepr, TypeRepr)] =
  if t <:< TypeRepr.of[Map[?, ?]] then
    appliedArgs(t) match
      case k :: v :: Nil => Some((k, v))  // Got key and value
      case _ => report.errorAndAbort(s"Map requires two type args: ${t.show}")
  else None

What’s happening:

  • Check if it’s a Map[Something, Something]
  • Extract BOTH type arguments: key and value
  • If Map has wrong number of args, abort (compiler error)

Examples:

1
2
3
Map[String, Int]  // ✅ Returns Some((String, Int))
Map[Long, User]   // ✅ Returns Some((Long, User))
String            //  Returns None

Inspector 6: Can this be a Map key?

1
2
3
4
5
6
7
def isAtomicKey(t: TypeRepr): Boolean =
  t =:= TypeRepr.of[String] ||
  t =:= TypeRepr.of[Int] ||
  t =:= TypeRepr.of[Long] ||
  t =:= TypeRepr.of[Short] ||
  t =:= TypeRepr.of[Byte] ||
  t =:= TypeRepr.of[Boolean]

Why this matters: Maps in Spark StructType only support primitive keys. You can’t have Map[User, String] - User isn’t a primitive.

Examples:

1
2
3
Map[String, User]  // ✅ String is atomic
Map[Int, Data]     // ✅ Int is atomic
Map[User, String]  //  User is NOT atomic - compile error!

The pattern recap:

Each inspector has ONE job:

  1. isCaseClass - “Is this a case class?”
  2. appliedArgs - “What are the generic type arguments?”
  3. optionArg - “Is this an Option, and what’s inside?”
  4. seqArg - “Is this a sequence, and what’s the element type?”
  5. mapArgs - “Is this a Map, and what are key/value types?”
  6. isAtomicKey - “Can this be a Map key?”

Compose these to answer complex questions. That’s the TypeInspector pattern.


## Part 6 - Building shapes recursively: The interpreter pattern

Now we use those inspectors to build a TypeShape - our internal representation of a type’s structure.

This is recursive. For List[Option[User]], we build:

❌ REMOVED: Recursive shape example (click to see what was removed)
1
2
3
4
5
SequenceShape(
  OptionalShape(
    StructShape([field1, field2, ...])
  )
)

Let me show you the algorithm step by step.

❌ REMOVE entire Part 6 recursive algorithm walkthrough

Why remove: Complete Teaching section with step-by-step algorithm walkthrough. “Let me show you the algorithm step by step” is classic Teaching narration.

What this served in Teaching version (~550 words):

  • “Now we use those inspectors…” (Teaching progression)
  • Recursive tree example showing structure
  • “The shape building algorithm:” flowchart
  • Algorithm implementation with inline comments
  • “Walk through an example: List[Option[String]]” (Teaching demonstration)
  • Line-by-line recursion walkthrough with bullet points
  • “Let’s break this down line by line:” (Teaching technique)
  • Case class handling with numbered explanations
  • Multiple mermaid diagrams showing execution flow

Why it doesn’t fit Resource:

  • Resource readers need the implementation, not the walkthrough
  • “Let me show you” is Teaching language
  • Step-by-step recursion trace is a learning exercise
  • “Let’s break this down” explains code line-by-line
  • If readers can’t understand recursion, Prerequisites failed
  • Multiple pedagogical diagrams (how it works vs what it does)

What to KEEP (if anything): The core typeShapeOf implementation code as a reference, but without the walkthrough. However, this might be internal implementation detail not needed in Resource version.

Word count removed: ~550 words

❌ REMOVED: Part 6 complete content - algorithm flowchart, code walkthrough, recursion example (~550 words - click to see what was removed)

The shape building algorithm:

flowchart TD
    T["typeShapeOf(t)"] --> O{"Option?"}
    O -- "Yes" --> OI["inner = optionArg(t)"] --> R1["typeShapeOf(inner)"]
    O -- "No" --> S{"Sequence?"}
    S -- "Yes" --> SE["elem = seqArg(t)"] --> R2["SequenceShape(typeShapeOf(elem))"]
    S -- "No" --> M{"Map?"}
    M -- "Yes" --> MKV["(k,v) = mapArgs(t)"]
    MKV --> K{"isAtomicKey(k)?"}
    K -- "No" --> ERR["abort: unsupported key"]
    K -- "Yes" --> MV["MapShape(PrimitiveShape(k), typeShapeOf(v))"]
    M -- "No" --> CC{"Case class?"}
    CC -- "Yes" --> FS["StructShape(collect FieldShape...)"]
    CC -- "No" --> P["PrimitiveShape(t.show)"]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def typeShapeOf(t: TypeRepr): TypeShape =
  // Step 1: Is it an Option?
  optionArg(t).map(typeShapeOf).getOrElse {

    // Step 2: Is it a sequence?
    seqArg(t).map(a => SequenceShape(typeShapeOf(a))).getOrElse {

      // Step 3: Is it a Map?
      mapArgs(t).map { case (k, v) =>
        if !isAtomicKey(k) then
          report.errorAndAbort(s"Unsupported Map key: ${k.show}")
        MapShape(PrimitiveShape(k.show), typeShapeOf(v))
      }.getOrElse {

        // Step 4: Is it a case class?
        if isCaseClass(t) then
          // ... handle case class
        else
          // Step 5: Fallback - primitive
          PrimitiveShape(t.show)
      }
    }
  }

The order matters! We check Option first, then sequences, then maps, then case classes, finally primitives.

Walk through an example: List[Option[String]]

flowchart TD
    A["List[Option[String]]"] -->|typeShapeOf| B{"Option?"}
    B -- "No" --> C{"Sequence?"}
    C -- "Yes" --> D["elem = Option[String]"]
    D --> E["typeShapeOf(Option[String])"]
    E --> F{"Option?"}
    F -- "Yes" --> G["inner = String"]
    G --> H["typeShapeOf(String)"]
    H --> I{"Case class?"}
    I -- "No" --> J["PrimitiveShape(String)"]
    J --> K["SequenceShape(PrimitiveShape(String))"]
  • Step 1: Is List[Option[String]] an Option?

    • No. optionArg returns None.
    • Continue to Step 2.
  • Step 2: Is it a sequence?

    • Yes! seqArg returns Some(Option[String]).
    • Build SequenceShape(typeShapeOf(Option[String])).
    • Recurse on Option[String].
  • Recursion Step 1: Is Option[String] an Option?

    • Yes! optionArg returns Some(String).
    • Recurse on String.
  • Recursion Step 2: Is String an Option?

    • No.
  • Recursion Step 3: Is String a sequence?

    • No.
  • Recursion Step 4: Is String a Map?

    • No.
  • Recursion Step 5: Is String a case class?

    • No.
  • Recursion Step 6: Fallback to primitive.

    • Return PrimitiveShape("String").

Unwind the recursion:

1
2
3
PrimitiveShape("String")
   (from Option recursion)
   SequenceShape(PrimitiveShape("String"))

Wait, where did the Option go? It got consumed! The typeShapeOf for Option just recurses on the inner type. This is the edge case I mentioned - element-level optionality gets lost.

Handling case classes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if isCaseClass(t) then
  val sym    = t.typeSymbol                      // Get the class symbol
  val params = sym.primaryConstructor.paramSymss.flatten  // Get constructor params

  val fields = params.map { p =>
    val name       = p.name                      // Field name
    val ptpe       = t.memberType(p)             // Field type
    val hasDefault = p.flags.is(Flags.HasDefault) // Has default value?
    val (uT, isOpt) = optionArg(ptpe).fold(ptpe -> false)(a => a -> true)

    FieldShape(name, typeShapeOf(uT), hasDefault, isOpt)
  }

  StructShape(fields)

Let’s break this down line by line:

  1. t.typeSymbol - Get the symbol for the case class
  2. sym.primaryConstructor - Case classes have a primary constructor
  3. .paramSymss.flatten - Get all parameters (flatten handles multiple param lists)
  4. For each parameter p:
    • p.name - The field name: "id", "email", etc.
    • t.memberType(p) - The field’s type as TypeRepr
    • p.flags.is(Flags.HasDefault) - Check if it has a default value like age: Int = 0
    • optionArg(ptpe).fold(...) - Check if the field type is Option[T]:
      • If yes: extract T, set isOpt = true
      • If no: use the type as-is, set isOpt = false
    • typeShapeOf(uT) - Recurse! Build the shape for the field’s underlying type

Example: case class User(id: Long, email: String, age: Option[Int] = None)

flowchart TD
    U["User(id: Long, email: String, age: Option[Int] = None)"]
    U --> F1["Field 'id' : Long"]
    F1 --> S1["PrimitiveShape(Long)"]
    U --> F2["Field 'email' : String"]
    F2 --> S2["PrimitiveShape(String)"]
    U --> F3["Field 'age' : Option[Int] (default None)"]
    F3 --> O1{"Option?"}
    O1 -- "Yes" --> A1["inner = Int"]
    A1 --> S3["PrimitiveShape(Int)"]
    S1 --> COLLECT["Collect fields"]
    S2 --> COLLECT
    S3 --> COLLECT
    COLLECT --> FINAL["StructShape([ id:PrimitiveShape(Long), email:PrimitiveShape(String), age:PrimitiveShape(Int) (optional, default) ])"]

Processing:

  • Field 1: id

    • name = “id”
    • ptpe = TypeRepr for Long
    • hasDefault = false
    • optionArg(Long) = None → uT = Long, isOpt = false
    • typeShapeOf(Long) = PrimitiveShape("Long")
    • Result: FieldShape("id", PrimitiveShape("Long"), hasDefault=false, isOptional=false)
  • Field 2: email

    • name = “email”
    • ptpe = TypeRepr for String
    • hasDefault = false
    • optionArg(String) = None → uT = String, isOpt = false
    • typeShapeOf(String) = PrimitiveShape("String")
    • Result: FieldShape("email", PrimitiveShape("String"), hasDefault=false, isOptional=false)
  • Field 3: age

    • name = “age”
    • ptpe = TypeRepr for Option[Int]
    • hasDefault = true (has = None)
    • optionArg(Option[Int]) = Some(Int) → uT = Int, isOpt = true
    • typeShapeOf(Int) = PrimitiveShape("Int")
    • Result: FieldShape("age", PrimitiveShape("Int"), hasDefault=true, isOptional=true)
  • Final shape:

    1
    2
    3
    4
    5
    
    StructShape([
      FieldShape("id", PrimitiveShape("Long"), false, false),
      FieldShape("email", PrimitiveShape("String"), false, false),
      FieldShape("age", PrimitiveShape("Int"), true, true)
    ])

Why this matters:

Now we have a normalized representation of the case class. We can compare two StructShapes to see if they match, regardless of the original Scala syntax.

The recursive structure handles arbitrarily nested types:

1
2
3
4
5
case class Order(
  items: List[LineItem],           // Recurses into List, then into LineItem
  metadata: Map[String, String],   // Recurses into Map, then String (twice)
  address: Option[Address]         // Recurses into Option, then Address
)

Each recursion builds a piece of the tree. The tree is the schema.


Part 7 - Field vs Element optionality: A critical distinction

Here’s something subtle that most guides miss. The code in github handles two kinds of optionality:

⭕ KEEP Part 7 optionality gotcha

Decision: Keep Part 7 with minor edits - remove Teaching preamble, keep technical content.

Why it fits Resource:

  • Gotcha/edge case documentation (critical for production)
  • “Most guides miss” → experts appreciate knowing pitfalls
  • Specific bug: List[Option[String]] getting flattened
  • Includes workaround and warning admonition
  • Visual diagram showing the issue

Why this is Resource, not Teaching:

  • Not teaching concepts, warning about bugs
  • Production safety content
  • Experts need to know limitations before deploying

Minor edit needed: Remove “Here’s something subtle…” preamble (Teaching tone).

Word count kept: ~250 words

The code in github handles two kinds of optionality:

Field-level optionality (Option on the field itself)

1
2
case class User(name: String, age: Option[Int])
//                                   ^^^^^^^^^^^ field-level Option

The macro detects this and sets isOptional = true on the FieldShape:

1
2
val (uT, isOpt) = optionArg(ptpe).fold(ptpe -> false)(a => a -> true)
FieldShape(name, typeShapeOf(uT), hasDefault, isOpt)

Element-level optionality (Option inside a collection)

1
2
case class Data(items: List[Option[String]])
//                          ^^^^^^^^^^^^^^ element-level Option

This creates a nested shape: SequenceShape(OptionShape(PrimitiveShape("String")))

Wait, that’s not in the code in github! Let me check… Actually, the code in github’s typeShapeOf handles this by recursing:

1
2
3
optionArg(t).map(typeShapeOf).getOrElse {
  seqArg(t).map(a => SequenceShape(typeShapeOf(a))).getOrElse { ... }
}

So List[Option[String]] becomes:

  1. seqArg detects List → extract Option[String]
  2. Recurse on Option[String]typeShapeOf(Option[String])
  3. optionArg detects Option → extract String
  4. Base case: PrimitiveShape("String")

But the final shape is SequenceShape(PrimitiveShape("String")) - the intermediate Option is consumed!

Why this matters for contracts: When you write:

1
2
case class Contract(items: List[String])
case class Producer(items: List[Option[String]])

These should probably NOT conform under Exact. The producer allows null elements, the contract doesn’t. But the current code in github might allow it because the Option gets unwrapped.

This is the kind of edge case you discover when shipping to production. The fix would be to add an OptionalShape wrapper in the model.

Optionality edge case
The element-level optionality issue (List[Option[String]] getting flattened) is real and subtle. If your schemas use nullable collections, test that the compile-time check actually catches the difference between List[String] and List[Option[String]]. The current implementation may allow mismatches here. Add explicit test cases for your nullable collection patterns before relying on them in production.

Visual (field vs element optionality):

Field vs Element Optionality

Field optionality vs element optionality - they're not the same.

flowchart LR
    A["List[Option[String]]"] --> B{"Element optionality?"}
    B -- "intended shape" --> C["SequenceShape(OptionalShape(PrimitiveShape(String)))"]
    B -- "current code in github\n(consumes Option)" --> D["SequenceShape(PrimitiveShape(String))"]

Part 8 - Compile‑time “conforms” evidence (the full picture)

Now let’s put it all together. Here’s the complete macro from the code in github:

⭕ KEEP Part 8 Conforms macro (remove walkthrough)

Decision: Keep the macro implementation code, remove “Let me break down” walkthrough.

Why implementation fits Resource:

  • This IS the core API - the Conforms macro
  • Complete, working implementation
  • Production-ready code
  • Copy-paste ready

What to REMOVE:

  • “Now let’s put it all together” (Teaching progression)
  • “Let me break down what’s happening:” section (Teaching walkthrough)
  • Numbered explanations of each step

Why walkthrough doesn’t fit Resource:

  • Experts read macro code directly
  • “Let me break down” is Teaching language
  • If readers can’t understand this, Prerequisites failed

Word count: Keep ~120 words (code), Remove ~150 words (walkthrough)

Here’s the complete macro from the code in github:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/main/scala/com/example/Conforms.scala
package com.example

import scala.quoted.*

final class Conforms[Out, Contract, P <: Policy]

object Conforms:
  inline given materialize[Out, Contract, P <: Policy](using Shape[Out], Shape[Contract]): Conforms[Out, Contract, P] =
    ${ conformsImpl[Out, Contract, P] }

  private def conformsImpl[Out: Type, Contract: Type, P: Type](using Quotes): Expr[Conforms[Out, Contract, P]] =
    import quotes.reflect.*

    def shapeOf[A: Type]: List[(String, String, Boolean, Boolean)] =
      val sh = Expr.summon[Shape[A]].getOrElse(report.errorAndAbort("No Shape available"))
      '{
        val fs = $sh.fields
        fs.map(f => (f.name, f.tpe, f.hasDefault, f.isOptional))
      }.valueOrAbort

    val out      = shapeOf[Out]
    val contract = shapeOf[Contract]

    // Compare fields by name (unordered, case-sensitive for now)
    val outMap      = out.map { case (n,t,_,opt) => n -> (t,opt) }.toMap
    val contractMap = contract.map { case (n,t,_,opt) => n -> (t,opt) }.toMap

    val missing = contract.collect { case (n,t,_,opt) if !outMap.contains(n) => s"$n:$t${if opt then " (optional)" else ""}" }
    val extra   = out.collect      { case (n,_,_,_) if !contractMap.contains(n) => n }
    val mism    = contract.collect {
      case (n,t,_,opt) if outMap.get(n).exists(_._1 != t)  => s"$n expected $t, found ${outMap(n)._1}"
      case (n,_,_,opt) if outMap.get(n).exists(_._2 != opt) => s"$n optionality mismatch"
    }

    val (okMissing, okExtra, okMism) = Type.of[P] match
      case '[Policy.Exact]    => (missing, extra, mism)
      case '[Policy.Backward] => (missing.filterNot(_.contains("(optional)")), Nil, mism)
      case '[Policy.Forward]  => (Nil, extra, mism)
      case _                  => (missing, extra, mism)

    if okMissing.nonEmpty || okExtra.nonEmpty || okMism.nonEmpty then
      val msg = s"""
        |Compile‑time contract drift (policy: ${Type.show[P]}).
        |Out: ${Type.show[Out]} vs Contract: ${Type.show[Contract]}
        |Missing: ${okMissing.mkString(", ")}
        |Extra: ${okExtra.mkString(", ")}
        |Mismatched: ${okMism.mkString("; ")}
        |""".stripMargin
      quotes.reflect.report.errorAndAbort(msg)
    else '{ new Conforms[Out, Contract, P] }

Let me break down what’s happening:

1. Extract shapes: We summon Shape[Out] and Shape[Contract] and get their field lists. Note the .valueOrAbort - this forces compile-time evaluation.

2. Compare: We build maps of field names → (type, optionality) and find differences.

3. Apply policy: - Exact: all differences are errors - Backward: missing optional fields are OK, extras are OK - Forward: extras are OK (but missing required fields are not)

4. Abort or succeed: If there are violations, we call report.errorAndAbort with a detailed message showing exactly what’s wrong. Otherwise, we return the evidence.

The beauty here: this runs during compilation. If schemas don’t match, your code won’t compile. Period.

❌ REMOVE numbered walkthrough

Why remove: “Let me break down what’s happening:” is classic Teaching technique. Resource readers read the macro code directly.

What this served: Step-by-step explanation of macro logic for learners.

Why it doesn’t fit Resource: Experts understand macro implementations without numbered guides.

Word count removed: ~150 words

Build breakage alert
Compile-time contract checks will break your build when schemas drift. That’s the point, but coordinate with your team. When upstream adds a field, every downstream pipeline build fails until they update contracts or transforms. This forces alignment, but can block deployments if not managed. Use CI branch builds to preview impact before merging schema changes.
⭕ KEEP warning admonition

Decision: Keep the “Build breakage alert” admonition.

Why it fits Resource:

  • Production gotcha/warning
  • Practical deployment advice
  • Team coordination guidance

Word count kept: ~50 words

Use it like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/test/scala/com/example/ConformsSpec.scala
package com.example

final case class Contract(id: Long, email: String)
final case class OutOk(id: Long, email: String)
final case class OutMissing(id: Long)

@main def checkConformance(): Unit =
  summon[Conforms[OutOk,      Contract, Policy.Exact]]   // ✅ compiles
  // summon[Conforms[OutMissing, Contract, Policy.Exact]] // ❌ compile‑time error (Missing: email:String)
  summon[Conforms[OutMissing, Contract, Policy.Backward]] //  compiles (migration mode)
⭕ KEEP usage example

Decision: Keep the usage code example.

Why it fits Resource:

  • Shows API usage clearly
  • Success and failure cases
  • Copy-paste ready
  • No Teaching narration, just code

Word count kept: ~40 words (code)

That’s it. The entire core. In production, you’d want to handle the element optionality issue I mentioned, but the structure stays the same.

❌ REMOVE conclusion paragraph

Why remove: “That’s it. The entire core.” is Teaching summary. Resource readers don’t need narration.

Word count removed: ~25 words


## Part 9 - Developer ergonomics: What using this actually feels like

❌ REMOVE entire Part 9 developer ergonomics

Why remove: Complete Teaching section about “what it feels like” to use the tool. Resource readers want the API, not subjective experiences.

What this served in Teaching version (~350 words):

  • “Let’s talk about the practical side…” (Teaching narrative)
  • Compile times discussion (answering learner concerns)
  • Error messages walkthrough (showing what it looks like)
  • Integration patterns (Spark, schema registries, CI/CD examples)
  • Onboarding guidance (“New team members ask…”)
  • “When to use vs skip” decision matrix

Why it doesn’t fit Resource:

  • “What using this actually feels like” is Teaching empathy
  • Resource doesn’t discuss feelings, just facts
  • Integration examples belong in dedicated Integration section (kept elsewhere)
  • “When to use” is for deciding, Resource assumes you decided
  • Onboarding guidance is for managers, not engineers using the API

Word count removed: ~350 words

❌ REMOVED: Part 9 complete content - ergonomics, compile times, integration patterns (~350 words - click to see what was removed)

Let’s talk about the practical side - what happens when you actually use this in your daily work?

Compile times

Question: Does this slow down compilation?

In practice, not noticeably. The macro runs once per summon[Conforms[...]] call during compilation. For a typical pipeline with 5-10 contract checks, you’re adding maybe 100-200ms to your build. Compare that to the hours you’d spend debugging a production schema mismatch.

Error messages

When schemas drift, you get this:

1
2
3
4
5
[error] Compile-time contract drift (policy: Policy.Exact).
[error] Out: CustomerProducer vs Contract: CustomerContract
[error] Extra: segment
[error] Missing:
[error] Mismatched:

Clear, actionable, and it stops your build. No cryptic macro errors, no stack traces. Just: “Hey, you have an extra field called segment.”

Integration patterns

With Spark:

1
2
3
4
5
6
7
8
// Your Spark job
val df = spark.read.parquet(path)
val typed = df.as[CustomerProducer]  // Spark encoder

// Compile-time check before transformation
summon[Conforms[CustomerProducer, CustomerContract, Policy.Exact]]

val output = typed.transform(dropSegment).as[CustomerContract]

With schema registries:

1
2
3
4
// Avro schema in registry → case class → contract check
case class CustomerAvro(id: Long, email: String, amt: Double)
summon[Conforms[CustomerAvro, CustomerContract, Policy.Exact]]
// Build fails if Avro schema drift!

CI/CD integration: Just run sbt compile. If contracts drift, the build fails. No special tooling needed. Works with Jenkins, GitHub Actions, GitLab CI - anything that runs sbt.

Onboarding

New team members ask: “Why won’t this compile?”

Answer: “Check the error. You’re trying to use CustomerProducer but the contract expects CustomerContract. Either adjust your transform or update the contract.”

That’s it. The compiler guides them. After one or two cases, they get it.

When to use vs skip

Use compile-time contracts when:

  • Multiple teams work on the same pipeline codebase
  • Schema changes break production regularly
  • You want fast feedback (compile vs deploy-and-test)

Skip when:

  • One-off scripts or exploratory work
  • External APIs you can’t control (use runtime validation)
  • Schema is genuinely dynamic (JSON with unknown keys)

Part 10 - Nested types: Maps, Lists, and deep structures

The code in github handles nested types beautifully. Look at this example from CtdcPoc.scala:

⭕ KEEP Part 10 nested types example (remove walkthrough)

Decision: Keep the code example, remove “What’s happening here:” walkthrough.

Why example fits Resource:

  • Working code showing nested structures
  • Real-world patterns (Maps, Lists, nested case classes)
  • Copy-paste ready

What to REMOVE:

  • “The code in github handles nested types beautifully” (unnecessary praise)
  • “What’s happening here:” numbered walkthrough

Word count: Keep ~60 words (code), Remove ~100 words (walkthrough)

Look at this example from CtdcPoc.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Deep / Nested structures
final case class LineItem(sku: String, qty: Int, attrs: Map[String, String])
final case class Address(street: String, zip: String)

final case class OrderOut(
  id: Long,
  items: List[LineItem],
  shipTo: Option[Address],
  tags: Set[String]
)

final case class OrderContract(
  id: Long,
  items: Seq[LineItem],        // List vs Seq - both are sequences
  shipTo: Option[Address],      // Nested case class
  tags: Seq[String] = Nil       // Set vs Seq, has default
)

val evDeepOk: SchemaConforms[OrderOut, OrderContract, SchemaPolicy.Backward.type] = summon

What’s happening here:

1. Nested case classes - Address inside OrderOut. The macro recurses: when it sees Option[Address], it unwraps to Address, then checks if Address is a case class, and recurses again to get its fields.

2. Collections - List[LineItem] vs Seq[LineItem]. Both match because seqArg treats them equivalently. The macro sees “sequence of LineItem” and recurses on LineItem.

3. Maps - Map[String, String] in attrs. The macro checks key type (must be atomic), then recurses on value type.

4. Default values - tags: Seq[String] = Nil has a default. Under Backward policy, if OrderOut is missing tags, it’s OK because the contract has a default.

This is why the TypeInspector pattern matters - it handles arbitrary nesting without special cases.

❌ REMOVE 'What's happening here' walkthrough

Decision: Strike through the numbered explanation walkthrough.

Why remove:

  • Teaching technique: “What’s happening here:” followed by step-by-step explanation
  • Explains how the macro works internally (recurses, unwraps, treats equivalently)
  • Resource readers can read the code example and understand the nested types capability
  • The explanation of TypeInspector pattern is Teaching content

Word count removed: ~115 words


Part 10 - Policy modifiers in two minutes (Ordered, CI, By‑Position)

Sometimes you need stricter comparison. Or different rules. Here are three quick policy extensions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// src/main/scala/com/example/PolicyMods.scala
package com.example

sealed trait PolicyMod
object PolicyMod:
  sealed trait ExactOrdered   extends PolicyMod // names + order + types must match
  sealed trait ExactCI        extends PolicyMod // case‑insensitive field names
  sealed trait ExactByPosition extends PolicyMod // ignore names; types by index
  case object ExactOrdered    extends ExactOrdered
  case object ExactCI         extends ExactCI
  case object ExactByPosition extends ExactByPosition

object ConformsMod:
  import scala.quoted.*
  inline def apply[Out, Contract, M <: PolicyMod](using Shape[Out], Shape[Contract]): Unit = ${ impl[Out, Contract, M] }

  private def impl[Out: Type, Contract: Type, M: Type](using Quotes): Expr[Unit] =
    import quotes.reflect.*
    def fields[A: Type]: List[(String,String)] =
      val sh = Expr.summon[Shape[A]].getOrElse(report.errorAndAbort("No Shape available"))
      '{ $sh.fields.map(f => (f.name, f.tpe)) }.valueOrAbort

    val out      = fields[Out]
    val contract = fields[Contract]

    val mismatches: List[String] = Type.of[M] match
      case '[PolicyMod.ExactCI] =>
        val n = (s: String) => s.toLowerCase
        val om = out.map{ case (n0,t) => n(n0) -> t }.toMap
        val cm = contract.map{ case (n0,t) => n(n0) -> t }.toMap
        (cm.keySet union om.keySet).toList.flatMap { k =>
          (cm.get(k), om.get(k)) match
            case (Some(t1), Some(t2)) if t1 != t2 => List(s"$k expected $t1, found $t2")
            case (Some(_), None) => List(s"missing ${k}")
            case (None, Some(_)) => List(s"extra ${k}")
            case _ => Nil
        }
      case '[PolicyMod.ExactByPosition] =>
        if out.length != contract.length then List(s"length mismatch: ${out.length} vs ${contract.length}")
        else
          out.zip(contract).zipWithIndex.collect {
            case (((_,tO),(_,tC)), i) if tO != tC => s"@$i expected $tC, found $tO"
          }
      case '[PolicyMod.ExactOrdered] =>
        if out.length != contract.length then List(s"length mismatch: ${out.length} vs ${contract.length}")
        else
          out.zip(contract).zipWithIndex.flatMap {
            case (((nO,tO),(nC,tC)), i) =>
              val nameOk = if nO == nC then Nil else List(s"@$i(name) expected $nC, found $nO")
              val typeOk = if tO == tC then Nil else List(s"@$i(type) expected $tC, found $tO")
              nameOk ++ typeOk
          }
      case _ => report.errorAndAbort("Unknown policy mod")

    if mismatches.nonEmpty then
      report.errorAndAbort(s"Modifier drift: ${mismatches.mkString("; ")}")
    else '{ () }

Try these:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.example.*

final case class A(id: Long, name: String)
final case class B(name: String, id: Long)

ConformsMod[A, B, PolicyMod.ExactByPosition] // ✅ same types by index
// ConformsMod[A, B, PolicyMod.ExactOrdered] // ❌ order matters → name mismatch

final case class CIa(ID: Long)
final case class CIb(id: Long)
ConformsMod[CIa, CIb, PolicyMod.ExactCI] //  caseinsensitive names match

Why these three? Because real systems need them: - ExactOrdered: For formats where position matters (some CSV parsers, protobuf with field numbers) - ExactCI: Because someone always uses UserId when the contract says userId - ExactByPosition: For legacy systems that ignore field names entirely

⭕ KEEP Part 10 Policy modifiers (remove motivation explanation)

Decision: Keep the policy modifier code and examples, remove “Why these three?” explanation.

Why keep the code:

  • API extension patterns showing how to add custom policies
  • Three concrete policy variants with implementation
  • Usage examples showing when each compiles vs fails
  • This is reference documentation for advanced usage

Why remove “Why these three?”:

  • Teaching motivation explaining “Because real systems need them”
  • Justification for design decisions (Teaching content)
  • Resource readers can understand from code and usage examples

Word count removed: ~40 words


Part 11 - Phantom types: Building impossible-to-misuse APIs

Here’s where things get interesting. The code, also uses phantom types, type-indexing / typestate builder patterns to build a pipeline builder that’s impossible to misuse. What are phantom types? They’re type parameters that don’t appear in the class body but control what you can do with an instance. Think of them as compile-time state machines.

⭕ KEEP Part 11 Phantom types (remove teaching explanations)

Decision: Keep Part 11 - it demonstrates advanced production pattern, but remove Teaching explanations.

Why keep:

  • Advanced production pattern (Phantom Type Builder Pattern)
  • Shows compile-time state machine enforcement
  • Demonstrates integration with SchemaConforms contract checking
  • References to authoritative sources (Xebia, Rhetorical Musings)
  • Production rationale about enforcing operation order

Why remove some content:

  • “Here’s where things get interesting” - Teaching enthusiasm
  • “What are phantom types?” definition - Teaching 101
  • “Think of them as…” - Teaching mental model

Strategy: Strike through introductory teaching, keep code, diagram, and production discussion.

stateDiagram-v2
    [*] --> Empty
    Empty --> WithSource: addSource
    WithSource --> WithTransform: transformAs[Next]
    WithTransform --> Complete: addSink[Contract,P] + conforms
    Complete --> [*]: build

    note right of WithTransform
      Evidence constraints:
      - S <: WithSource to transform
      - S <: WithTransform to sink
      - S = Complete to build
    end note

From the codebase SparkCore.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Phantom type states
sealed trait BuilderState
sealed trait Empty         extends BuilderState
sealed trait WithSource    extends BuilderState
sealed trait WithTransform extends BuilderState
sealed trait Complete      extends BuilderState

final case class PipelineBuilder[S <: BuilderState, CurContract] private (
  name: String,
  steps: List[PipelineStep]
):
  // Can only add source when Empty
  def addSource[C](src: TypedSource[C])(using SparkSchema[C]): PipelineBuilder[WithSource, C] =
    PipelineBuilder[WithSource, C](name, steps :+ ...)

  // Can only transform after adding source
  def transformAs[Next](f: DataFrame => DataFrame)(using
    ev: S <:< WithSource,  // This constraint enforces order!
    sch: SparkSchema[Next]
  ): PipelineBuilder[WithTransform, Next] =
    PipelineBuilder[WithTransform, Next](name, steps :+ ...)

  // Can only add sink after transform
  def addSink[R, P <: SchemaPolicy](sink: TypedSink[R])(using
    ev0: S <:< WithTransform,  // Must have transform
    ev1: SchemaConforms[CurContract, R, P],  // Compile-time contract check!
    sch: SparkSchema[R]
  ): PipelineBuilder[Complete, CurContract] =
    PipelineBuilder[Complete, CurContract](name, steps :+ ...)

  // Can only build when Complete
  def build(using ev: S =:= Complete): SparkSession => DataFrame =
    (spark: SparkSession) => ...

object PipelineBuilder:
  def apply[CurContract](name: String): PipelineBuilder[Empty, CurContract] =
    PipelineBuilder[Empty, CurContract](name, Nil)

### How this works:

1. State transitions as types - Each method returns a new type parameter S. The compiler tracks which state you’re in.

2. Evidence constraints - ev: S <:< WithSource means “S must be a subtype of WithSource”. If it’s not, this method doesn’t exist.

3. Compile-time state machine - You literally cannot call methods in the wrong order:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ✅ This compiles
PipelineBuilder[Contract]("good")
  .addSource(src)
  .transformAs[Next](transform)
  .addSink[Contract, SchemaPolicy.Exact](sink)
  .build

// ❌ This doesn't compile - can't transform before adding source
PipelineBuilder[Contract]("bad")
  .transformAs[Next](transform)  // ERROR: No implicit evidence S <:< WithSource

4. Contract checking embedded - Notice ev1: SchemaConforms[CurContract, R, P] in addSink? That’s where the compile-time contract check happens. The pipeline literally won’t build if schemas drift.

❌ REMOVE 'How this works' walkthrough

Decision: Strike through the numbered walkthrough, keep the usage examples.

Why remove:

  • “How this works:” - Teaching walkthrough structure
  • Numbered explanations of state transitions, evidence constraints
  • “Notice…?” - Teaching call-out technique
  • “That’s where…” - Teaching explanation of mechanism

Why keep usage examples:

  • Shows what compiles vs what doesn’t
  • Concrete code demonstrating the API
  • Error messages document expected behavior

Word count removed: ~95 words

This pattern is called the Phantom Type Builder Pattern. For more details, see:

Why this matters: In production data pipelines, the order of operations matters. Read → Transform → Validate → Write. With phantom types, the compiler enforces this order. You can’t accidentally write before transforming. You can’t transform without a source.

This is the difference between “here’s how macros work” and “here’s how to build production contract systems."

❌ REMOVE closing teaching explanation

Decision: Strike through “Why this matters” and philosophical closing.

Why remove:

  • “Why this matters:” - Teaching motivation pattern
  • Explains the importance and benefits (Teaching technique)
  • “This is the difference between…” - Meta-commentary about teaching itself
  • Resource readers can understand the value from the code and references

Word count removed: ~60 words

Note: References to Xebia and Rhetorical Musings blog posts are KEPT as they’re Resource links.


Part 12 - Schema evolution and versioning: Handling change over time

Here’s the reality: Schemas evolve. Fields get added, deprecated, renamed. Teams roll out changes incrementally. The question isn’t whether schemas will change - it’s how you manage those changes without breaking production.

⭕ KEEP Part 12 Schema evolution (remove teaching scenario)

Decision: Keep Part 12 - it documents production migration strategies and versioning patterns.

Why keep:

  • Production migration strategies (Phase 1/2/3 deployment)
  • Versioning patterns with code examples
  • Handling breaking changes (field renames)
  • Coexistence patterns during migration
  • Complete real-world migration example from codebase
  • Mermaid diagram showing migration phases

Why remove some content:

  • “Here’s the reality:” - Teaching preamble
  • “The question isn’t whether…” - Teaching philosophy
  • Wrong/Right approach contrast - Teaching technique showing anti-patterns first
  • “What’s happening here:” - Teaching walkthrough
  • “This is real-world stuff” - Teaching enthusiasm

Strategy: Strike through Teaching framing, keep all technical content, versioning strategies, and code.

The schema evolution problem

Scenario: You have CustomerV1 running in production. Marketing wants to add a segment field for targeting. But you have 10 pipelines reading CustomerV1. How do you evolve without breaking everything?

Wrong approach: Add segment to CustomerV1, deploy, hope for the best. Result: Some pipelines break because they don’t handle the new field.

Right approach: Version your contracts and use policies to manage transitions.

❌ REMOVE teaching scenario setup

Decision: Strike through the scenario, wrong/right approach contrast.

Why remove:

  • Teaching technique: presents problem scenario first
  • Shows anti-pattern (“Wrong approach”) before solution
  • Conversational “How do you…” question to reader
  • Resource readers want the solution directly, not the motivation

Word count removed: ~60 words

Versioning strategy

1
2
3
4
5
6
7
// contracts/CustomerV1.scala - Current production
package contracts
final case class CustomerV1(id: Long, email: String)

// contracts/CustomerV2.scala - New version with segment
package contracts
final case class CustomerV2(id: Long, email: String, segment: Option[String] = None)

Notice segment is: 1. Optional (Option[String]) 2. Has a default (= None)

This makes it backward-compatible - old code can work with new data.

❌ REMOVE 'Notice...' teaching callout

Decision: Strike through the “Notice…” explanation.

Why remove:

  • “Notice…” - Teaching call-out technique
  • Numbered list explaining design choices
  • Explains “why” (backward compatibility) - Teaching motivation
  • Resource readers can see Option[String] = None in the code

Word count removed: ~25 words

Migration phases

Phase 1: Add the field (Backward policy)

Deploy new producers writing CustomerV2:

1
2
3
4
5
val producer = PipelineBuilder[CustomerV2]("write-v2")
  .addSource(rawData)
  .transformAs[CustomerV2](addSegment)
  .addSink[CustomerV2, Policy.Backward](sink)  // Backward allows optional fields
  .build

Old consumers still read CustomerV1. They ignore the segment field. No breakage.

Phase 2: Migrate consumers

Update each consumer one by one:

1
2
3
4
5
val consumer = PipelineBuilder[CustomerV2]("read-v2")
  .addSource[CustomerV2](source)
  .transformAs[EnrichedCustomer](useSegment)  // Now uses segment field
  .addSink[EnrichedCustomer, Policy.Exact](sink)
  .build

The compile-time contract ensures: “Does this consumer handle the new schema?” If you forget to update the case class, it won’t compile.

Phase 3: Deprecate V1

Once all pipelines use CustomerV2:

1
2
3
// Mark V1 as deprecated
@deprecated("Use CustomerV2", "2025-10-01")
final case class CustomerV1(id: Long, email: String)

The compiler warns any remaining V1 usage. After a grace period, delete V1 entirely.

❌ REMOVE teaching narrative from migration phases

Decision: Strike through teaching narrative around the phase code examples.

Why remove:

  • “Deploy new producers…” - Teaching instructions
  • “Old consumers still read…” - Teaching explanation of what happens
  • “Update each consumer one by one:” - Teaching step-by-step
  • “The compile-time contract ensures:…” - Teaching explanation with rhetorical question
  • “Once all pipelines…” - Teaching narrative
  • “The compiler warns… After a grace period…” - Teaching explanation

Why keep:

  • Phase structure (Phase 1/2/3) - Documentation organization
  • Code examples showing producer, consumer, deprecation patterns
  • Inline comments in code (they’re part of the code)

Word count removed: ~70 words

Handling breaking changes

What if you need to rename a field? Say, emailemailAddress?

Non-breaking approach:

1
2
3
4
5
6
7
// Step 1: Add the new field, keep the old
final case class CustomerV3(
  id: Long,
  email: String,              // Old field
  emailAddress: String,       // New field
  segment: Option[String] = None
)

Wait, that’s duplication. Better:

1
2
3
4
5
6
7
8
9
// Step 1: Add alias constructor
final case class CustomerV3(
  id: Long,
  emailAddress: String,       // New canonical name
  segment: Option[String] = None
)
object CustomerV3:
  def fromV2(v2: CustomerV2): CustomerV3 =
    CustomerV3(v2.id, v2.email, v2.segment)

Step 2: Migrate producers to write emailAddress Step 3: Migrate consumers to read emailAddress Step 4: Remove the fromV2 constructor

At each step, compile-time contracts verify: “Does this transformation produce the expected schema?"

❌ REMOVE teaching questions and narrative

Decision: Strike through teaching questions and step labels, keep code examples.

Why remove:

  • “What if you need…?” - Teaching rhetorical question
  • “Non-breaking approach:” - Teaching label
  • “Wait, that’s duplication. Better:” - Teaching self-correction dialogue
  • “Step 2/3/4” labels - Teaching step-by-step instructions
  • “At each step…” - Teaching explanation

Why keep:

  • Code examples showing both approaches (duplication vs alias constructor)
  • Inline comments explaining field roles

Word count removed: ~45 words

Coexistence during migration

During migration, you have both V2 and V3 running. How to handle?

1
2
3
4
5
6
7
8
sealed trait CustomerSchema
case class V2(id: Long, email: String, segment: Option[String] = None) extends CustomerSchema
case class V3(id: Long, emailAddress: String, segment: Option[String] = None) extends CustomerSchema

// Transformation handles both
def normalize(schema: CustomerSchema): V3 = schema match
  case V2(id, email, segment) => V3(id, email, segment)
  case v3: V3 => v3

The contract checks both paths:

1
2
summon[Conforms[V2, V3Contract, Policy.Backward]]  // V2 → V3 allowed
summon[Conforms[V3, V3Contract, Policy.Exact]]     // V3  V3 exact match
❌ REMOVE teaching question and explanation

Decision: Strike through teaching question and explanation, keep code.

Why remove:

  • “How to handle?” - Teaching rhetorical question
  • “The contract checks both paths:” - Teaching explanation of what the code does

Word count removed: ~15 words

Real-world migration: Backward → Exact

Let’s look at a complete migration scenario from the code in github. This is exactly how you’d roll out schema changes in production.

From CtdcPoc.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Sink contract: the target schema we promise to write. */
final case class CustomerContract(id: Long, email: String, age: Option[Int] = None)

/** Producer: imagine an upstream source adds an extra field `segment`. */
final case class CustomerProducer(id: Long, email: String, age: Option[Int], segment: String)

/** Declared "Next" schema after a transform (dropping `segment`). */
final case class CustomerNext(id: Long, email: String, age: Option[Int])

// Step 1: Migration phase - use Backward policy
// Producer has extra field, but contract allows it during migration
val srcB = TypedSource[CustomerProducer]("csv", inPath, Map("header" -> "true"))
val sinkB = TypedSink[CustomerContract](tmpOutB)

val dropExtras: DataFrame => DataFrame = _.select($"id", $"email", $"age")

val planB =
  PipelineBuilder[CustomerContract]("CSV -> Parquet B: transformAs[CustomerNext], Exact")
    .addSource(srcB)
    .transformAs[CustomerNext]("drop segment")(dropExtras)
    .addSink[CustomerContract, SchemaPolicy.Exact.type](sinkB)
    .build

### What’s happening here:

Phase 1: Producer adds field - Upstream adds segment field to CustomerProducer - Our contract is still CustomerContract (no segment) - We use transformAs[CustomerNext] to explicitly drop the extra field - Then check CustomerNext conforms to CustomerContract under Exact

Why this works: 1. The transform explicitly declares the output schema (CustomerNext) 2. The compiler checks: Does CustomerNext match CustomerContract? Yes (both have id, email, age) 3. If we later change CustomerNext by accident, compilation fails

Phase 2: Stabilization Once the migration is done:

1
2
3
4
5
6
7
// Later: Everyone uses CustomerContract, no transform needed
val planStable =
  PipelineBuilder[CustomerContract]("stable")
    .addSource(contractSource)
    .noTransform  // Direct pass-through
    .addSink[CustomerContract, SchemaPolicy.Exact.type](sink)
    .build

This is real-world stuff. The code shows you exactly how to migrate schemas safely.

❌ REMOVE 'What's happening here' walkthrough

Decision: Strike through the entire “What’s happening here:” section, keep code examples.

Why remove:

  • “What’s happening here:” - Teaching walkthrough structure
  • Bulleted explanations of each step
  • “Why this works:” - Teaching explanation with numbered breakdown
  • “Once the migration is done:” - Teaching narrative
  • “This is real-world stuff. The code shows you…” - Teaching enthusiasm

Why keep:

  • Code example showing CustomerProducerCustomerNext transform
  • Code example showing stabilized pipeline with noTransform
  • Mermaid diagram showing migration flow
  • Inline code comments

Word count removed: ~110 words

flowchart TD
    P1["Producer adds field 'segment'"] --> T1["transformAs[CustomerNext]\n(drop extras)"]
    T1 --> V1["summon Conforms[CustomerNext,CustomerContract,Policy.Exact]"]
    V1 --> W1["write sink"]

    subgraph Phase 1 ["Migration window"]
      direction TB
      P1 --> T1 --> V1 --> W1
    end

    W1 --> P2["Stabilize: sources match contract"]
    P2 --> V2["Conforms[CustomerContract,CustomerContract,Policy.Exact]"]
    V2 --> W2["noTransform + write"]

Part 13 - Testing compile‑time, for real (copy/paste tests)

How do you test that code fails to compile? With assertDoesNotCompile:

⭕ KEEP Part 13 Testing patterns (remove teaching question)

Decision: Keep Part 13 - it shows testing patterns for compile-time contracts.

Why keep:

  • Testing patterns with assertDoesNotCompile / assertCompiles
  • Complete test suite example
  • CI/CD integration (GitHub Actions YAML)
  • Mermaid diagram showing test flow
  • Production testing best practices

Why remove some content:

  • “How do you…?” - Teaching rhetorical question
  • “This is gold” - Teaching enthusiasm

Strategy: Strike through teaching questions and enthusiasm, keep all test code and CI config.

sequenceDiagram
    participant Dev
    participant CI as "GitHub Actions"
    participant SBT as "sbt testOnly *CompileTimeSpec"
    Dev->>CI: open PR
    CI->>SBT: run compile-fail suite
    SBT->>SBT: assertCompiles / assertDoesNotCompile
    alt all pass
        SBT-->>CI: success
        CI-->>Dev: green check
    else drift detected
        SBT-->>CI: failure with errorAndAbort msg
        CI-->>Dev: red X + compiler diff
    end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/test/scala/com/example/CompileTimeSpec.scala
package com.example
import org.scalatest.funsuite.AnyFunSuite

class CompileTimeSpec extends AnyFunSuite:
  test("Exact should fail when a required field is missing") {
    assertDoesNotCompile("summon[Conforms[OutMissing, Contract, Policy.Exact]]")
  }
  test("Backward should pass for the same mismatch") {
    assertCompiles("summon[Conforms[OutMissing, Contract, Policy.Backward]]")
  }
  test("By‑Position should accept re‑ordered names with same types") {
    assertCompiles("ConformsMod[A, B, PolicyMod.ExactByPosition]")
  }
  test("Ordered should reject the same re‑ordering") {
    assertDoesNotCompile("ConformsMod[A, B, PolicyMod.ExactOrdered]")
  }
  test("Pipeline builder enforces correct order") {
    assertDoesNotCompile("""
      PipelineBuilder[Contract]("bad")
        .transformAs[Next](identity)  // Can't transform without source
    """)
  }
  test("Nested types conform correctly") {
    assertCompiles("summon[Conforms[OrderOut, OrderContract, Policy.Backward]]")
  }

You can wire this into CI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# .github/workflows/compile-fail.yml (concept)
name: compile-fail
on: [pull_request]
jobs:
  cf:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '17' }
      - name: Compile‑fail suite
        run: sbt -v "testOnly *CompileTimeSpec"

This is gold. Your CI literally proves that broken schemas can’t be deployed.

❌ REMOVE teaching enthusiasm

Decision: Strike through enthusiastic closing statement.

Why remove:

  • “This is gold” - Teaching enthusiasm
  • “Your CI literally proves…” - Teaching explanation of benefit
  • Resource readers can understand CI value from the test code and YAML config

Word count removed: ~12 words


Part 14 - Scala 2: how to do the same (and what’s different)

Scala 2 uses def macros with a Context (blackbox/whitebox). The idea is identical, but the implementation uses c.universe trees instead of Expr/quotes.

⭕ KEEP Part 14 Scala 2 implementation (remove teaching preamble)

Decision: Keep Part 14 - it shows Scala 2 variant of the API.

Why keep:

  • Scala 2 macro implementation (API variant for different Scala version)
  • Complete code showing blackbox Context approach
  • Key differences table (Scala 3 vs Scala 2)
  • Reference to Magnolia for cross-version derivation

Why remove some content:

  • Opening sentence explaining “The idea is identical…” - Teaching comparison
  • “Scala 3 macros are safer…” - Teaching judgment about trade-offs

Strategy: Strike through teaching comparisons, keep code and differences table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// build.sbt (S2 module)
// scalaVersion := "2.13.14"
// libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value

// src/main/scala-2/com/example/ConformsS2.scala
package com.example
import scala.reflect.macros.blackbox

final class ConformsS2[Out, Contract, P]

object ConformsS2 {
  implicit def materialize[Out, Contract, P <: Policy](implicit sOut: Shape[Out], sCon: Shape[Contract]): ConformsS2[Out, Contract, P] =
    macro conformsImpl[Out, Contract, P]

  def conformsImpl[Out: c.WeakTypeTag, Contract: c.WeakTypeTag, P: c.WeakTypeTag](c: blackbox.Context)(sOut: c.Expr[Shape[Out]], sCon: c.Expr[Shape[Contract]]): c.Expr[ConformsS2[Out, Contract, P]] = {
    import c.universe._

    def fieldsOf[T: c.WeakTypeTag](sh: c.Expr[Shape[T]]): List[(String,String,Boolean,Boolean)] = {
      c.eval(c.Expr[List[(String,String,Boolean,Boolean)]](q"$sh.fields.map(f => (f.name, f.tpe, f.hasDefault, f.isOptional))"))
    }

    val out      = fieldsOf[Out](sOut)
    val contract = fieldsOf[Contract](sCon)

    val outMap      = out.map{ case (n,t,_,opt) => n -> (t,opt)}.toMap
    val contractMap = contract.map{ case (n,t,_,opt) => n -> (t,opt)}.toMap

    val missing = contract.collect { case (n,t,_,opt) if !outMap.contains(n) => s"$n:$t${if (opt) " (optional)" else ""}" }
    val extra   = out.collect      { case (n,_,_,_) if !contractMap.contains(n) => n }
    val mism    = contract.collect {
      case (n,t,_,_) if outMap.get(n).exists(_._1 != t)  => s"$n expected $t, found ${outMap(n)._1}"
      case (n,_,_,o) if outMap.get(n).exists(_._2 != o)  => s"$n optionality mismatch"
    }

    if (missing.nonEmpty || extra.nonEmpty || mism.nonEmpty)
      c.abort(c.enclosingPosition, s"Drift: missing=${missing.mkString(",")} extra=${extra.mkString(",")} mism=${mism.mkString(";")}")
    else
      c.Expr[ConformsS2[Out, Contract, P]](q"new _root_.com.example.ConformsS2[${weakTypeOf[Out]}, ${weakTypeOf[Contract]}, ${weakTypeOf[P]}]")
  }
}

Key differences:

  • Scala 3: inline + ${ ... } splices; Scala 2: macro def with a Context
  • Error reporting: report.errorAndAbort (Scala 3) vs c.abort (Scala 2)
  • Trees: Expr[T] (Scala 3) vs raw Tree (Scala 2)
  • Type inspection: TypeRepr (Scala 3) vs Type (Scala 2)

Scala 3 macros are safer. The quoted API prevents many common macro bugs. But Scala 2 macros work fine if you’re careful.

❌ REMOVE teaching judgment

Decision: Strike through teaching comparison about safety.

Why remove:

  • “Scala 3 macros are safer” - Teaching judgment/opinion
  • “But… if you’re careful” - Teaching advice
  • Resource readers can see differences in the table above

Word count removed: ~20 words

For derivation, check out Magnolia - it works across both versions.


Part 15 - Integration with Spark: Runtime defense-in-depth

The code in github also shows how to mirror compile-time policies with Spark’s built-in structural comparators for runtime validation.

⭕ KEEP Part 15 Spark integration (remove teaching preamble)

Decision: Keep Part 15 - it shows integration pattern with Spark.

Why keep:

  • Integration pattern with Spark StructType
  • Runtime policy mapping code
  • Two-layer validation strategy (compile-time + runtime)
  • Mermaid diagram showing policy mapping
  • Reference to Spark DataType API docs

Why remove:

  • “The code in github also shows how…” - Teaching narrative introduction

Strategy: Strike through teaching introduction, keep all integration code and strategy.

From SparkCore.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Derive Spark StructType from case class at compile time
trait SparkSchema[C]:
  def struct: StructType

object SparkSchema:
  inline given derived[C]: SparkSchema[C] = ${ sparkSchemaImpl[C] }

  // Macro that converts case class → StructType
  private def sparkSchemaImpl[C: Type](using Quotes): Expr[SparkSchema[C]] = ...

// Runtime policy mapping to Spark comparators
trait PolicyRuntime[P <: SchemaPolicy]:
  def ok(found: StructType, expected: StructType): Boolean

object PolicyRuntime:
  given PolicyRuntime[SchemaPolicy.Exact.type] with
    def ok(found: StructType, expected: StructType) =
      DataType.equalsIgnoreCaseAndNullability(found, expected)

  given PolicyRuntime[SchemaPolicy.ExactByPosition.type] with
    def ok(found: StructType, expected: StructType) =
      DataType.equalsStructurally(found, expected, ignoreNullability = true)

  given PolicyRuntime[SchemaPolicy.ExactOrdered.type] with
    def ok(found: StructType, expected: StructType) =
      DataType.equalsStructurallyByName(found, expected, _ == _)

Two-layer validation strategy:

  1. Compile-time (macros) - Catches schema drift before deployment
  2. Runtime (Spark comparators) - Defensive check for external data sources
flowchart TD
    F["found: StructType"] --- E["expected: StructType"]
    F --> EX["Policy.Exact"] --> C1["equalsIgnoreCaseAndNullability(found,expected)"]
    F --> EP["Policy.ExactByPosition"] --> C2["equalsStructurally(found,expected,true)"]
    F --> EO["Policy.ExactOrdered"] --> C3["equalsStructurallyByName(found,expected,resolver)"]

For more on Spark’s structural comparators, see the DataType API docs .


Part 16 - A tiny migration playbook you can adopt tomorrow

Here’s how I use these policies in practice:

⭕ KEEP Part 16 Migration playbook (remove personal framing)

Decision: Keep Part 16 - it’s production best practices and migration playbook.

Why keep:

  • Concrete policy usage patterns (Critical paths → Exact, etc.)
  • Production best practices (pure transformations, I/O at edges, idempotency)
  • Pithy summary quote about compile-time vs runtime

Why remove:

  • “Here’s how I use…” - Personal/teaching framing
  • “And remember:” - Teaching instruction tone
  • “you can adopt tomorrow” in heading - Teaching promise

Strategy: Strike through personal framing, keep all technical guidance.

  • Critical paths → Exact. No surprises. If schemas don’t match exactly, builds fail.
  • Producer adding optional fields → Backward during rollout. Allows new optional fields on the producer side.
  • Consumer that can ignore extras → Forward during rollout. Lets the consumer tolerate extra fields.
  • After stabilization → tighten back to Exact.

And remember:

  • Keep transformations pure (no side effects inside map/filter)
  • Place I/O at edges (read once, write once)
  • Make side-effects idempotent (so retries don’t duplicate writes)

Compile-time prevents drift; runtime manages reality. Tests catch behavior, not shapes.


Part 17 - Production patterns: What I learned shipping this

⭕ KEEP Part 17 Production patterns (remove personal framing)

Decision: Keep Part 17 100% - this is RESOURCE GOLD per FINAL-EXECUTION-PLAN.

Why keep:

  • Pattern 1: Contract organization by version
  • Pattern 2: Schema caching with companion objects
  • Pattern 3: Inline policy documentation
  • Pattern 4: Version-controlled compile-fail tests
  • All patterns are concrete, production-ready best practices

Why remove:

  • “What I learned shipping this” - Personal teaching narrative in heading

Strategy: Strike through personal framing in heading only, keep all 4 patterns intact.

Pattern 1: Organize contracts by version

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// contracts/CustomerV1.scala
package contracts
final case class CustomerV1(id: Long, email: String)

// contracts/CustomerV2.scala
package contracts
final case class CustomerV2(id: Long, email: String, name: Option[String] = None)

// pipelines/CustomerMigration.scala
import contracts._

val migration = PipelineBuilder[CustomerV2]("v1-to-v2")
  .addSource[CustomerV1](sourceV1)
  .transformAs[CustomerV2](addNameField)
  .addSink[CustomerV2, SchemaPolicy.Exact](sinkV2)
  .build

Pattern 2: Use companion objects for schema caching

1
2
3
case class User(id: Long, email: String)
object User:
  given SparkSchema[User] = summon[SparkSchema[User]]  // Compute once, reuse

Pattern 3: Document policy choices inline

1
2
3
4
5
// Good: Explicit reasoning
.addSink[Contract, SchemaPolicy.Backward](sink)  // Allow optional fields during Q2 migration

// Bad: No context
.addSink[Contract, SchemaPolicy.Full](sink)  // Why Full? When will we tighten?

Pattern 4: Test your compile-fail cases

1
2
3
4
// Keep these in version control as documentation
test("CustomerV1 should not conform to CustomerV2 under Exact") {
  assertDoesNotCompile("summon[Conforms[CustomerV1, CustomerV2, Policy.Exact]]")
}

Appendix - Full demo project skeleton (copy & run)

⭕ KEEP Appendix - Runnable project skeleton

Decision: Keep Appendix - runnable code skeleton is perfect Resource content.

Why keep:

  • Complete project directory structure
  • build.sbt configuration for Scala 3
  • Run command
  • Tip for Scala 2 variant

No removals needed: This section is pure Resource content (copy-paste-ready setup).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.
├── build.sbt
└── src
    ├── main
    │   └── scala
    │       └── com/example/
    │           ├── IntroMacro.scala
    │           ├── Shape.scala
    │           ├── Policy.scala
    │           ├── Conforms.scala
    │           └── SparkCore.scala
    └── test
        └── scala
            └── com/example/
                ├── ShapeSpec.scala
                ├── ConformsSpec.scala
                └── CompileTimeSpec.scala

build.sbt (Scala 3)

1
2
3
4
5
6
7
ThisBuild / scalaVersion := "3.3.3"
ThisBuild / organization := "com.example"

libraryDependencies ++= Seq(
  "org.apache.spark" %% "spark-sql" % "3.5.1" % "provided",
  "org.scalatest" %% "scalatest" % "3.2.17" % Test
)

Run it:

1
sbt "runMain ctdc.CtdcPoc"

Tip: For Scala 2, add a sibling module with scala‑reflect and copy the S2 file from above.


References

⭕ KEEP References - Essential links

Decision: Keep References section - essential Resource content.

Why keep:

  • Curated links to Scala 3 macros resources (Rock the JVM, official docs)
  • Apache Spark integration documentation
  • Phantom types articles
  • Magnolia library reference

No removals needed: Reference sections are pure Resource content.

Scala 3 Macros & Metaprogramming

Apache Spark Integration

Phantom Types & Type-Level Programming

Other Resources


What compile-time contracts can’t catch

Let’s be honest about the boundaries. Compile-time contracts handle a lot, but they’re not magic. Here’s what they can’t do:

⭕ KEEP Limitations table (remove teaching preamble)

Decision: Keep limitations/gotchas table - critical Resource content.

Why keep:

  • Comprehensive table of what compile-time contracts can’t catch
  • Categories: External data drift, late-arriving changes, data quality, partial failures
  • Solutions column provides runtime validation alternatives
  • “The takeaway” summary about defense-in-depth

Why remove:

  • “Let’s be honest about the boundaries” - Teaching conversational tone
  • “they’re not magic” - Teaching demystification

Strategy: Strike through teaching preamble, keep table and takeaway.

CategoryWhat it can’t catchWhySolution
External data drift• Third-party APIs that change without telling you; • Kafka topics from teams you don’t coordinate with; • S3 buckets written by external vendorsYou don’t control their build processUse runtime validation (schema registries, data contracts)
Late-arriving schema changes• Schema registry updated after your deploy; • Database columns added while your job runsCompile-time happens before deployRuntime checks with version monitoring
Data quality issues• Nulls where you expect values (even if field is non-optional); • Out-of-range numbers (age = -5); • Malformed strings (email without @)Compile-time checks structure, not contentGreat Expectations, Deequ, or custom validators
Non-breaking additions• Upstream adds optional field you don’t use yetThis is often fine! Forward policy handles itPolicy-based awareness or stricter monitoring
Partial batch failures• 1000 records match schema, 5 don’tCompile-time is binary (compiles or doesn’t)Runtime validation with error tables/quarantine

The takeaway: Compile-time contracts catch drift within your controlled codebase during development and deployment. For everything else - external sources, runtime changes, data quality - layer in runtime validation. Think defense-in-depth: compile-time as the first gate, runtime as the safety net.


TL;DR

⭕ KEEP TL;DR Summary

Decision: Keep TL;DR - concise summary is perfect Resource content.

Why keep:

  • Bulleted summary of key concepts and patterns
  • No teaching narrative, just technical takeaways
  • Covers: compile-time evidence, TypeInspector, phantom types, optionality, migrations, Spark integration, limitations
  • Each bullet is a concrete technical point

No removals needed: This is already concise Resource-style summary.

  • Compile-time evidence + policy types make schema intent explicit and enforceable
  • TypeInspector pattern organizes type inspection into composable utilities
  • Phantom types with type-indexing/type-state builders create impossible-to-misuse APIs through compile-time state machines
  • Field vs element optionality matters for nested types
  • Nested structures (Maps, Lists, case classes) work through recursive shape building
  • Real migrations: use Backward during rollout, tighten to Exact after stabilization
  • Spark’s comparators provide runtime defense-in-depth
  • Schema evolution needs versioning, coexistence strategies, and policy-based transitions
  • Developer ergonomics matter: clear errors, fast compile times, easy onboarding
  • Compile-time doesn’t replace runtime validation - it complements it for controlled systems
  • This catches many drift classes early, but you still need runtime checks for external data
  • Compile-time contracts don’t make pipelines unbreakable - they make breakage predictable
Vitthal Mirji profile photo

Vitthal Mirji

Staff Data Engineer @ Walmart

Mumbai, India

Staff Data Engineer & Architect from Mumbai, India. Sharing insights on Data Engineering, Functional programming, Scala, Open source, and life.

Expertise
  • Data Engineering
  • Scala
  • Apache Spark
  • Functional Programming
  • Cloud Architecture
  • GCP
  • Big Data
Next time, we'll talk about "Kafka: Because sometimes you need 47 ways to say 'message queue'"