Featured image of post Building a Reusable Financial Data Pipeline with DuckDB — View Consolidation Pattern & Subscription Monetization

Building a Reusable Financial Data Pipeline with DuckDB — View Consolidation Pattern & Subscription Monetization

Deep dive into DuckDB financial data pipeline architecture: VIEW consolidation for multi-source data, Python class-based reusable engine, from bank statements to monthly P&L reports. Build a ¥299/month subscription product.

DuckDB Financial Data Pipeline Architecture

Why Financial Data Pipelines Are a Product Goldmine

Data analysts have a shared pain point: every new project starts from zero code.

Client A needs financial reports — you write CSV reading, aggregation, and report generation code. Client B also needs it — you copy and tweak paths. Client C wants invoice data added — you add another table. Three months later, you have a dozen “similar but different” projects, each requiring separate maintenance.

The root cause isn’t bad code — it’s missing reusable architecture.

DuckDB’s VIEW consolidation pattern + Python class encapsulation lets you write an engine once and reuse it across N clients. This article breaks down the architecture and how to turn it into a ¥299/month × N revenue product.


1. Architecture Overview: Three-Layer Data Pipeline

A reusable financial data pipeline has three layers:

┌─────────────────────────────────────────────┐
│  Layer 1: Data Ingestion                    │
│  bank_transactions.csv → read_csv           │
│  invoices.csv       → read_csv              │
│  expenses.csv       → read_csv              │
└──────────────────┬──────────────────────────┘
                   ▼
┌─────────────────────────────────────────────┐
│  Layer 2: View Consolidation                │
│  v_all_income   = UNION ALL (bank + invoices)│
│  v_all_expense  = UNION ALL (bank + platforms)│
└──────────────────┬──────────────────────────┘
                   ▼
┌─────────────────────────────────────────────┐
│  Layer 3: Analysis Engine                   │
│  P&L / Cash Flow / Tax / Trend Analysis     │
└─────────────────────────────────────────────┘

Key design principle: Layer 1 and Layer 2 are identical for every client. Only Layer 3 business rules need customization.

This means you only adjust analysis logic per client — no rewriting data ingestion and consolidation code.


2. Data Ingestion Layer: Multi-Source CSV in One SQL

Small businesses have data scattered everywhere: bank export CSVs, invoice system CSVs, Alipay/WeChat merchant platform CSVs. Traditional approach requires writing three different parsing scripts. With DuckDB, one SQL handles everything.

import duckdb
from pathlib import Path

class FinancialPipeline:
    """Reusable financial data pipeline engine"""
    
    def __init__(self, db_path: str):
        self.con = duckdb.connect(db_path)
        self._create_schema()
    
    def _create_schema(self):
        """Standard schema: shared across all projects"""
        self.con.execute("""
            -- Bank transactions: exported from online banking
            CREATE TABLE IF NOT EXISTS bank_transactions (
                date DATE,
                type VARCHAR,
                category VARCHAR,
                amount DECIMAL(12,2),
                counterparty VARCHAR,
                source VARCHAR
            )
        """)
        
        self.con.execute("""
            -- Invoice data: from invoice management system
            CREATE TABLE IF NOT EXISTS invoices (
                invoice_date DATE,
                invoice_number VARCHAR,
                seller VARCHAR,
                buyer VARCHAR,
                amount DECIMAL(12,2),
                tax_rate DECIMAL(5,4),
                status VARCHAR
            )
        """)
        
        self.con.execute("""
            -- Platform expenses: Alipay / WeChat / bank transfer
            CREATE TABLE IF NOT EXISTS platform_expenses (
                expense_date DATE,
                platform VARCHAR,
                category VARCHAR,
                amount DECIMAL(12,2),
                vendor VARCHAR,
                remark VARCHAR
            )
        """)

Architecture highlight: Schema defined once, reused across all projects. For new clients, just COPY ... FROM 'data/*.csv' to import data — no table structure changes needed.


3. View Consolidation Layer: UNION ALL for Unified Income/Expense口径

This is the core of the entire architecture. Different data sources have different semantics for “income” and “expense”:

  • Bank transactions with type='income' are income
  • Invoices with status='已收款' (collected) are also income
  • Alipay expenses and bank expenses are the same thing

Use VIEWs to unify them into the same口径:

-- Unified income view: bank income + collected invoices
CREATE OR REPLACE VIEW v_all_income AS
SELECT 
    date AS trans_date,
    'bank' AS source_type,
    category,
    amount,
    counterparty AS partner
FROM bank_transactions 
WHERE type = 'income'

UNION ALL

SELECT 
    invoice_date AS trans_date,
    'invoice_system' AS source_type,
    'sales' AS category,
    amount,
    buyer AS partner
FROM invoices 
WHERE status = 'collected';

-- Unified expense view: bank expenses + platform expenses
CREATE OR REPLACE VIEW v_all_expense AS
SELECT 
    date AS trans_date,
    'bank' AS source_type,
    category,
    amount,
    counterparty AS partner
FROM bank_transactions 
WHERE type = 'expense'

UNION ALL

SELECT 
    expense_date AS trans_date,
    'platform' AS source_type,
    category,
    amount,
    vendor AS partner
FROM platform_expenses;

Why VIEWs instead of physical tables?

  1. Real-time freshness: VIEWs read from base tables on every query — no re-ingest needed when new data arrives
  2. Storage efficient: No duplication, income and expense stored only once
  3. Centralized logic: Consolidation rules written once, all analysis queries benefit automatically
  4. Traceable: Each field has source_type, enabling data lineage tracking

4. Analysis Engine Layer: One-Click P&L Generation

With unified income/expense views, analysis logic becomes straightforward:

-- Monthly P&L statement
CREATE OR REPLACE VIEW v_monthly_pl AS
WITH monthly_income AS (
    SELECT 
        DATE_TRUNC('month', trans_date) AS month,
        SUM(amount) AS total_income,
        COUNT(*) AS transaction_count
    FROM v_all_income
    GROUP BY 1
),
monthly_expense AS (
    SELECT 
        DATE_TRUNC('month', trans_date) AS month,
        SUM(amount) AS total_expense,
        COUNT(*) AS transaction_count
    FROM v_all_expense
    GROUP BY 1
),
monthly_tax AS (
    -- Extract estimated taxes from invoices
    SELECT 
        DATE_TRUNC('month', invoice_date) AS month,
        SUM(amount * tax_rate) AS estimated_tax
    FROM invoices
    WHERE status IN ('issued', 'collected')
    GROUP BY 1
)
SELECT 
    i.month,
    ROUND(i.total_income, 2) AS revenue,
    ROUND(e.total_expense, 2) AS expenses,
    ROUND(i.total_income - e.total_expense, 2) AS gross_profit,
    ROUND((i.total_income - e.total_expense) / NULLIF(i.total_income, 0) * 100, 2) AS profit_margin_pct,
    ROUND(COALESCE(t.estimated_tax, 0), 2) AS estimated_tax,
    ROUND(i.total_income - e.total_expense - COALESCE(t.estimated_tax, 0), 2) AS net_profit,
    i.transaction_count + e.transaction_count AS total_transactions
FROM monthly_income i
LEFT JOIN monthly_expense e ON i.month = e.month
LEFT JOIN monthly_tax t ON i.month = t.month;

Add MoM and YoY growth (one SQL, no pandas merge needed):

-- Full P&L with MoM and YoY
SELECT 
    month,
    revenue,
    expenses,
    gross_profit,
    profit_margin_pct,
    -- Previous month (MoM)
    LAG(revenue) OVER w AS prev_month_revenue,
    ROUND(
        (revenue - LAG(revenue) OVER w) 
        / NULLIF(LAG(revenue) OVER w, 0) * 100, 2
    ) AS mom_growth_pct,
    -- Same month last year (YoY)
    LAG(revenue, 12) OVER w AS same_month_last_year,
    ROUND(
        (revenue - LAG(revenue, 12) OVER w) 
        / NULLIF(LAG(revenue, 12) OVER w, 0) * 100, 2
    ) AS yoy_growth_pct
FROM v_monthly_pl
WINDOW w AS (ORDER BY month);

5. Python Encapsulation: One Engine, N Clients

Complete reusable engine class:

class FinancialPipeline:
    """Financial data pipeline engine — write once, use N times"""
    
    def __init__(self, db_path: str):
        self.con = duckdb.connect(db_path)
        self._setup_views()
    
    def _setup_views(self):
        """View consolidation layer: shared across all projects"""
        # v_all_income and v_all_expense from above
        pass
    
    def ingest(self, month: str, data_dir: Path):
        """Data ingestion: called once per month"""
        for csv_file in data_dir.glob(f"{month}*.csv"):
            table_name = csv_file.stem
            self.con.execute(f"""
                COPY {table_name} FROM '{csv_file}' 
                (FORMAT CSV, HEADER, AUTO_DETERMINE)
            """)
        print(f"✅ {month} data ingested")
    
    def generate_report(self, month: str) -> pd.DataFrame:
        """Generate monthly P&L statement"""
        return self.con.execute(f"""
            SELECT * FROM v_monthly_pl 
            WHERE month = '{month}-01'::DATE
        """).fetchdf()
    
    def export_parquet(self, month: str, output_dir: Path):
        """Export as Parquet — 5x faster subsequent queries"""
        df = self.generate_report(month)
        df.to_parquet(output_dir / f"pl_{month}.parquet")
    
    def close(self):
        self.con.close()

Usage:

# Client A: Restaurant
pipeline_a = FinancialPipeline("clients/restaurant_01.duckdb")
pipeline_a.ingest("2026-07", Path("clients/restaurant_01/data"))
report_a = pipeline_a.generate_report("2026-07")
pipeline_a.export_parquet("2026-07", Path("clients/restaurant_01/output"))
pipeline_a.close()

# Client B: E-commerce — same engine, different data
pipeline_b = FinancialPipeline("clients/ecommerce_02.duckdb")
pipeline_b.ingest("2026-07", Path("clients/ecommerce_02/data"))
report_b = pipeline_b.generate_report("2026-07")
pipeline_b.close()

Key advantage: Client A and Client B use the same code. Each client has an independent DuckDB database file. Adding a new client = creating a new folder + importing data, zero code changes.


6. DuckDB vs Traditional Approach Comparison

DimensionDuckDB Pipelinepandas + ManualTraditional ETL
Multi-source CSV loadingOne SQL COPYMultiple pd.read_csv + mergeComplex configuration
VIEW consolidationNative SQL VIEWMultiple DataFrame mergesDepends on scheduler
MoM/YoY growthLAG() window functionNeed self-mergeCustom scripting
Engine reusabilityClass-based, zero-code reuseCopy-pasteRe-configuration needed
DeploymentZero (single .duckdb file)Requires Python envRequires server

7. Monetization Paths: From 0 to ¥10,000+/month

  • ¥299/client/month for automated monthly report generation
  • 5 clients = ¥1,495/month
  • 10 clients = ¥2,990/month
  • Marginal cost ≈ 0 (DuckDB processes 100K rows in < 1 second)

Path 2: Per-Project Fee

  • One-time fee ¥3,000-8,000 per client
  • Includes initial data migration + custom analysis logic
  • Ongoing maintenance billed separately at ¥500/month

Path 3: Productized SaaS

  • Wrap with FastAPI as a web application
  • Clients self-upload data, view reports in real-time
  • Price: ¥99-299/month/client
  • Multi-tenant support (each client gets independent DuckDB file)

Key insight: Financial reports are a must-have. Small businesses either hire an accountant (monthly salary ¥6,000-12,000) or buy accounting software (annual fee ¥2,000-5,000). Your DuckDB solution at ¥299/month is 1/10 the price of accounting software with equivalent results.


8. Production-Grade: Data Quality Checks

A real production pipeline needs data quality validation:

-- Data quality check view
CREATE OR REPLACE VIEW v_data_quality AS
SELECT 
    'bank_transactions' AS table_name,
    COUNT(*) AS total_rows,
    SUM(CASE WHEN amount IS NULL THEN 1 ELSE 0 END) AS null_amounts,
    SUM(CASE WHEN amount < 0 THEN 1 ELSE 0 END) AS negative_amounts,
    SUM(amount) AS total_amount
FROM bank_transactions

UNION ALL

SELECT 
    'invoices' AS table_name,
    COUNT(*) AS total_rows,
    SUM(CASE WHEN amount IS NULL THEN 1 ELSE 0 END),
    SUM(CASE WHEN amount < 0 THEN 1 ELSE 0 END),
    SUM(amount)
FROM invoices;

With quality checks, you can automatically validate data integrity before delivery. This is your charging justification — “I don’t just give you reports, I guarantee data quality”.


Summary

The core value of this architecture isn’t the technology itself — it’s reusability:

  1. VIEW consolidation pattern: Unifies multi-source data semantics, write rules once, all analyses benefit automatically
  2. Class-based engine: One FinancialPipeline class serves N clients, zero code for new clients
  3. DuckDB single file: Each client gets one .duckdb file — distribution, backup, and migration are trivial
  4. Subscription monetization: ¥299/month, marginal cost near zero, true passive income

After this article, you have all the skills needed to build financial data products. The next step is finding a real client and running the full pipeline with real data.

💡 More DuckDB pipeline patterns → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.