Domain Model Validation
Introduction
Welcome to the first lesson of "Building an Async CLI Tool for ETL Pipelines in Python", and congratulations on reaching this milestone! So far, you've completed four comprehensive courses in this learning path, building expertise in advanced Python techniques such as dunder methods, dataclasses, descriptors, metaclasses, functional patterns, pattern matching, and async I/O. That's a remarkable achievement, and you've earned the right to tackle this capstone project, where we'll integrate everything you've learned into a complete, production-ready command-line application.
Over the next five lessons, we'll build LedgerLift, a small but robust asynchronous ETL (Extract, Transform, Load) tool designed for processing financial transactions. This isn't a toy example; we'll implement industrial-strength patterns: self-validating domain models, streaming parsers, structural pattern matching for routing, async pipelines with natural backpressure, and a polished CLI interface. By the end, you'll have a portfolio piece that demonstrates advanced Python engineering skills.
Today's lesson focuses on the foundation: Domain Model & Validation. We'll design a robust, self-validating domain model using frozen, slotted dataclasses that enforce business rules at initialization time. You'll implement a Money value type with automatic currency normalization and decimal precision, create a reusable Range descriptor for constrained fields, and build a Transaction dataclass that integrates both. These components will validate every field, normalize inputs, and reject invalid data with clear error messages, ensuring that only well-formed transactions enter our pipeline. This validation layer is the bedrock of data quality; once it's in place, downstream components can operate with confidence, knowing the data is always correct. Let's begin by understanding why domain models matter in data pipelines.
Why Domain Models Matter in ETL Systems
ETL systems move data between systems: extracting from sources, transforming it, and loading it into destinations. The transformation step is where business logic lives, and that logic operates on a domain model: a structured representation of your business entities with their rules and constraints. Without a strong domain model, you're processing raw dictionaries or tuples, relying on scattered validation checks and hoping you didn't miss edge cases.
A well-designed domain model brings three critical benefits. First, centralized validation ensures that business rules are enforced in one place rather than scattered across parsing, transformation, and loading code. If a transaction requires a positive amount, that rule lives in the Transaction class itself; every code path that creates a transaction automatically enforces this constraint. Second, type safety gives you confidence that once a Transaction object exists, it's valid and complete. You don't need defensive checks throughout your codebase because the model guarantees correctness. Third, clear error boundaries mean that invalid data is rejected early with specific error messages. Instead of a cryptic failure deep in the pipeline, users see "amount must be > 0.00" at the entry point, where they can fix it.
In this lesson, we'll build domain models using frozen dataclasses with slots. Frozen dataclasses are immutable, which is perfect for data pipelines where records shouldn't change once created; this immutability prevents accidental modifications and makes the code easier to reason about. Slots reduce memory overhead by storing attributes in a fixed structure rather than a dictionary; when processing thousands of transactions, this efficiency adds up. The combination of frozen and slots gives us robust, efficient value types that feel native to Python.
