Back to Blog

Apache Nessie Deep Dive: Git-Like Data Version Control

Traditional data platforms face several critical challenges when managing data changes:

CoomiaPublished on November 11, 202517 min read
Share this articleTwitter / X

Series: S8 Technology Deep Dives · Article 3 | Level: Advanced | Reading Time: 20 min

Apache Nessie Deep Dive: Git-Like Data Version Control

#TL;DR

  • Apache Nessie brings Git-like version control to data lakehouses, enabling branch, merge, tag, and revert operations on data catalogs — making data changes as traceable and reversible as source code
  • Combined with Apache Iceberg, Nessie provides coomia-dip with cross-environment isolation (dev/staging/production), zero-downtime schema evolution, and millisecond-precision time-travel queries
  • This article dissects Nessie's internal architecture, five usage patterns within coomia-dip, Iceberg integration configuration, conflict resolution strategies, and multi-tenant versioning best practices

#1. Why Data Version Control Matters

#1.1 Pain Points in Traditional Data Management

Traditional data platforms face several critical challenges when managing data changes:

  • No traceability: Who changed which rows in which table, and when? Unanswerable
  • No rollback: A faulty ETL job overwrites production data, and there is no simple undo
  • Poor environment isolation: Development and production share the same data, creating high-risk change scenarios
  • Dangerous schema evolution: ALTER TABLE is an irreversible operation; failure means data corruption
  • Audit difficulties: Compliance mandates full auditability of data changes, but the platform lacks native support

These pain points become amplified in a multi-tenant PaaS environment like coomia-dip, where dozens of tenants share the same data infrastructure and a single erroneous write can cascade across organizational boundaries.

#1.2 The Git for Data Philosophy

Nessie applies Git's core concepts to data management:

Git ConceptNessie EquivalentData Scenario
BranchBranchIsolated workspace for data changes
CommitCommitAtomic snapshot of data modifications
TagTagVersion markers (e.g., month-end snapshots)
MergeMergePromote changes from development to main
DiffDiffCompare data differences between versions
Cherry-pickCherry-pickSelectively apply specific changes
RevertRevertRoll back to a historical version

The key insight is that data versioning operates at the catalog metadata level, not at the file level. Nessie tracks pointers to Iceberg metadata files, meaning branch creation is a constant-time operation regardless of data volume — creating a branch over a petabyte dataset takes the same 2-3 milliseconds as creating one over a megabyte dataset.

#1.3 Nessie's Role in coomia-dip

Code
┌────────────────────────────────────────────────┐
│              Data Layer (Data Layer)               │
│                                                  │
│  ┌──────────────────────────────────────────┐   │
│  │           Apache Nessie                   │   │
│  │        (Catalog Version Control)          │   │
│  │                                           │   │
│  │  main ──●──●──●──●──●──●──●── (prod)     │   │
│  │              \         /                  │   │
│  │    dev ───────●──●──●── (development)     │   │
│  │                  \                        │   │
│  │    etl-fix ───────●──● (hotfix)           │   │
│  └──────────────┬───────────────────────────┘   │
│                 │                                │
│  ┌──────────────▼───────────────────────────┐   │
│  │           Apache Iceberg                  │   │
│  │         (Table Format Layer)              │   │
│  │  ┌────────┐ ┌────────┐ ┌────────┐       │   │
│  │  │ Table A│ │ Table B│ │ Table C│       │   │
│  │  └────────┘ └────────┘ └────────┘       │   │
│  └──────────────┬───────────────────────────┘   │
│                 │                                │
│  ┌──────────────▼───────────────────────────┐   │
│  │        Object Storage (MinIO/S3)          │   │
│  └──────────────────────────────────────────┘   │
└────────────────────────────────────────────────┘

Within coomia-dip, Nessie serves as the version-control layer between the query engines (Doris, Spark, Flink) and the Iceberg table format. Every data mutation — whether from an ETL pipeline, a streaming ingestion job, or a manual data correction — flows through Nessie's commit model.

#2. Nessie Architecture Deep Dive

#2.1 Core Components

Code
┌─────────────────────────────────────┐
│            Nessie Server             │
│                                      │
│  ┌────────────────────────────────┐  │
│  │        REST API / gRPC API      │  │
│  │  (Catalog Operations Endpoint)  │  │
│  └──────────┬─────────────────────┘  │
│             │                        │
│  ┌──────────▼─────────────────────┐  │
│  │       Version Store Engine      │  │
│  │  (Ref Management, Merge Logic)  │  │
│  └──────────┬─────────────────────┘  │
│             │                        │
│  ┌──────────▼─────────────────────┐  │
│  │     Persist Backend             │  │
│  │  (RocksDB / DynamoDB / JDBC)    │  │
│  └────────────────────────────────┘  │
└─────────────────────────────────────┘

REST/gRPC API Layer: Exposes endpoints for branch creation, commit, merge, diff, and log operations. The API follows the Nessie specification v2, providing both REST and gRPC interfaces. In coomia-dip, internal services use gRPC for low-latency communication, while external tools (Spark, Flink) use the REST API.

Version Store Engine: The heart of Nessie. It implements a content-addressable storage model similar to Git's object database. Each commit is identified by a hash, and references (branches and tags) are mutable pointers to commits. The engine handles:

  • Reference resolution and update (compare-and-swap for conflict-free concurrent writes)
  • Merge logic with configurable conflict resolution
  • Commit graph traversal for log and diff operations
  • Garbage collection metadata tracking

Persist Backend: Nessie supports multiple storage backends for its version store:

BackendUse CaseThroughputDurability
RocksDBDevelopment, single-nodeHighLocal disk
PostgreSQL (JDBC)Small-to-medium productionMediumDatabase-level
DynamoDBAWS production, high scaleVery HighManaged
MongoDBSelf-hosted productionHighReplica set

For coomia-dip, we use PostgreSQL in self-hosted deployments and DynamoDB when running on AWS, providing a balance of operational simplicity and durability.

#2.2 The Content-Addressable Model

Nessie's internal data model revolves around three core concepts:

  1. Content Key: A hierarchical path identifying a data object (e.g., ontology_db.tenant_objects)
  2. Content Value: Metadata about the data object — for Iceberg tables, this includes the pointer to the current metadata.json file, the table UUID, and the schema ID
  3. Commit: An atomic set of content-key-to-value mappings, identified by a SHA-256 hash
Code
Commit abc123:
  ontology_db.orders → IcebergTable(metadata="/warehouse/orders/metadata/v42.metadata.json")
  ontology_db.users  → IcebergTable(metadata="/warehouse/users/metadata/v18.metadata.json")

Commit def456 (parent: abc123):
  ontology_db.orders → IcebergTable(metadata="/warehouse/orders/metadata/v43.metadata.json")
  # users unchanged — inherits from parent

This model means that branches are extremely lightweight: creating a branch simply creates a new named pointer to an existing commit hash. No data is copied.

#2.3 Concurrency Control

Nessie uses optimistic concurrency control via compare-and-swap (CAS) operations on branch HEAD pointers. When two concurrent writers attempt to commit to the same branch:

  1. Writer A reads HEAD = abc123, prepares commit with parent = abc123
  2. Writer B reads HEAD = abc123, prepares commit with parent = abc123
  3. Writer A issues CAS: update HEAD from abc123 to new_hash_A — succeeds
  4. Writer B issues CAS: update HEAD from abc123 to new_hash_Bfails (HEAD is now new_hash_A)
  5. Writer B retries: re-reads HEAD = new_hash_A, rebases changes, retries CAS

This model avoids locks entirely, providing high throughput for concurrent write workloads typical in ETL-heavy environments.

#2.4 Deployment Configuration for coomia-dip

YAML
# deployment-Layer/docker-compose/nessie.yml
services:
  nessie:
    image: ghcr.io/projectnessie/nessie:0.79.0
    ports:
      - "19120:19120"  # REST API
      - "19121:19121"  # Management
    environment:
      NESSIE_VERSION_STORE_TYPE: JDBC
      QUARKUS_DATASOURCE_URL: jdbc:postgresql://postgres:5432/nessie
      QUARKUS_DATASOURCE_USERNAME: nessie
      QUARKUS_DATASOURCE_PASSWORD: ${NESSIE_DB_PASSWORD}
      NESSIE_SERVER_DEFAULT_BRANCH: main
      NESSIE_SERVER_SEND_STACKTRACE_IN_ERROR_TO_CLIENT: false
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:19120/api/v2/config"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: "2.0"

Key tuning parameters:

ParameterDefaultRecommendedReason
nessie.version.store.persist.cache-capacity10000005000000More cached entries = fewer DB reads
nessie.version.store.persist.commit-batch-size2550Larger batches for ETL workloads
quarkus.datasource.jdbc.max-size2050Support more concurrent connections
JVM Heap512M2GPrevent GC pressure under load

#3. Five Usage Patterns in coomia-dip

#3.1 Pattern 1: Development/Staging/Production Isolation

The most fundamental pattern: each environment operates on its own Nessie branch. Data engineers develop and test ETL pipelines on the dev branch without any risk of affecting production data.

Python
# data-Layer/nessie/env_isolation.py
class EnvironmentIsolation:
    """Environment isolation via Nessie branches"""

    BRANCH_MAP = {
        "development": "dev",
        "staging": "staging",
        "production": "main",
    }

    def __init__(self, nessie_client):
        self.nessie = nessie_client

    def promote(self, from_env: str, to_env: str, message: str):
        """Promote data changes between environments"""
        from_branch = self.BRANCH_MAP[from_env]
        to_branch = self.BRANCH_MAP[to_env]

        # Dry-run: check for conflicts first
        diff = self.nessie.diff(from_ref=from_branch, to_ref=to_branch)
        if diff.has_conflicts:
            raise PromotionConflictError(
                f"Cannot promote {from_env} -> {to_env}: "
                f"{len(diff.conflicts)} conflicts detected"
            )

        self.nessie.merge(
            from_ref=from_branch,
            to_ref=to_branch,
            message=f"Promote {from_env} -> {to_env}: {message}",
        )

    def reset_dev_from_production(self):
        """Reset dev branch to match current production state"""
        main_ref = self.nessie.get_reference("main")
        self.nessie.assign_reference(
            name="dev",
            ref_type="BRANCH",
            new_hash=main_ref.hash,
        )

#3.2 Pattern 2: Atomic ETL with Branch-per-Job

Each ETL job executes on an ephemeral branch. If the job fails, the branch is simply discarded — production data remains untouched.

Python
# data-Layer/nessie/etl_branching.py
class AtomicETL:
    """Atomic ETL execution using Nessie branches"""

    def __init__(self, nessie_client, spark_session):
        self.nessie = nessie_client
        self.spark = spark_session

    def run_etl_atomically(self, job_name: str, etl_func):
        """Execute ETL on an isolated branch, merge on success"""
        branch_name = f"etl-{job_name}-{int(time.time())}"

        try:
            # 1. Create isolated branch from main
            main = self.nessie.get_reference("main")
            self.nessie.create_reference(
                name=branch_name,
                ref_type="BRANCH",
                source_ref=main.hash,
            )

            # 2. Execute ETL on the branch
            self.spark.conf.set("spark.sql.catalog.nessie.ref", branch_name)
            etl_func(self.spark)

            # 3. Validate data quality
            if not self._validate_data(branch_name):
                raise DataQualityError(f"ETL {job_name} quality check failed")

            # 4. Merge to main (atomic operation)
            self.nessie.merge(
                from_ref=branch_name,
                to_ref="main",
                message=f"ETL job: {job_name}",
            )

            # 5. Clean up working branch
            self.nessie.delete_reference(branch_name)

        except Exception as e:
            # On failure, discard branch — main is unaffected
            self.nessie.delete_reference(branch_name)
            raise ETLFailedError(f"ETL {job_name} failed: {e}")

#3.3 Pattern 3: Multi-Tenant Data Versioning

In coomia-dip's multi-tenant model, each tenant can independently snapshot and query historical versions of their data.

Python
# data-Layer/nessie/tenant_versioning.py
class TenantDataVersionManager:
    """Multi-tenant data version management"""

    def __init__(self, nessie_client):
        self.nessie = nessie_client

    def create_tenant_snapshot(self, tenant_id: str, label: str) -> str:
        """Create a tagged snapshot for a specific tenant"""
        tag_name = f"tenant-{tenant_id}-{label}"
        main_ref = self.nessie.get_reference("main")

        self.nessie.create_reference(
            name=tag_name,
            ref_type="TAG",
            source_ref=main_ref.hash,
        )
        return tag_name

    def query_tenant_at_version(self, tenant_id: str, tag_name: str) -> dict:
        """Query tenant data at a specific tagged version"""
        tag_ref = self.nessie.get_reference(tag_name)
        return {
            "ref": tag_name,
            "hash": tag_ref.hash,
            "query_hint": (
                f"SELECT * FROM nessie.ontology_objects "
                f"AT TAG '{tag_name}' "
                f"WHERE tenant_id = '{tenant_id}'"
            ),
        }

    def diff_tenant_versions(self, tag1: str, tag2: str):
        """Compare data differences between two tenant versions"""
        return self.nessie.diff(from_ref=tag1, to_ref=tag2)

#3.4 Pattern 4: Safe Schema Evolution

Schema changes are tested on an isolated branch before being merged to production. If validation fails, the branch is discarded with zero production impact.

Python
# data-Layer/nessie/schema_evolution.py
class SafeSchemaEvolution:
    """Use Nessie branches to safely execute schema changes"""

    def __init__(self, nessie_client, iceberg_catalog):
        self.nessie = nessie_client
        self.catalog = iceberg_catalog

    def evolve_schema_safely(self, table_name: str, schema_changes: list):
        """Test schema changes on an isolated branch"""
        branch = f"schema-evolution-{int(time.time())}"

        try:
            main = self.nessie.get_reference("main")
            self.nessie.create_reference(
                name=branch, ref_type="BRANCH", source_ref=main.hash
            )

            for change in schema_changes:
                self._apply_schema_change(branch, table_name, change)

            # Validate that existing queries still work
            self._validate_query_compatibility(branch, table_name)

            self.nessie.merge(
                from_ref=branch,
                to_ref="main",
                message=f"Schema evolution: {table_name}",
            )
        except Exception as e:
            self.nessie.delete_reference(branch)
            raise SchemaEvolutionError(f"Schema evolution failed: {e}")

#3.5 Pattern 5: Audit Compliance and Time Travel

Regulatory compliance demands full data change auditability. Nessie's commit log provides a complete, immutable audit trail.

Python
# data-Layer/nessie/audit_compliance.py
class DataAuditManager:
    """Data audit and compliance management"""

    def __init__(self, nessie_client):
        self.nessie = nessie_client

    def get_change_history(self, table_path: str, limit: int = 100) -> list:
        """Retrieve complete change history for a table"""
        log = self.nessie.get_log("main", limit=limit)
        table_changes = []

        for entry in log:
            for op in entry.operations:
                if op.key.elements == table_path.split("."):
                    table_changes.append({
                        "hash": entry.commit_meta.hash,
                        "author": entry.commit_meta.author,
                        "message": entry.commit_meta.message,
                        "timestamp": entry.commit_meta.commit_time,
                        "operation": op.type,
                    })
        return table_changes

    def create_compliance_snapshot(self, period: str) -> dict:
        """Create a tagged compliance snapshot (e.g., quarter-end)"""
        tag_name = f"compliance-{period}"
        main = self.nessie.get_reference("main")

        self.nessie.create_reference(
            name=tag_name, ref_type="TAG", source_ref=main.hash
        )
        return {
            "tag": tag_name,
            "hash": main.hash,
            "period": period,
            "tables": self._list_tables(main.hash),
        }

    def time_travel_query(self, table: str, timestamp: str) -> str:
        """Generate a time-travel query for a specific historical moment"""
        log = self.nessie.get_log("main")
        for entry in log:
            if entry.commit_meta.commit_time <= timestamp:
                commit_hash = entry.commit_meta.hash
                return f"SELECT * FROM nessie.{table} AT COMMIT '{commit_hash}'"
        raise ValueError(f"No commit found before {timestamp}")

#4. Iceberg Integration

#4.1 Spark + Nessie + Iceberg Configuration

Python
# data-Layer/spark/nessie_iceberg_config.py
from pyspark.sql import SparkSession

def create_spark_session_with_nessie() -> SparkSession:
    """Create a Spark session integrated with Nessie Catalog"""
    spark = (
        SparkSession.builder
        .appName("coomia-dip-data-pipeline")
        .config(
            "spark.jars.packages",
            "org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.0,"
            "org.projectnessie.nessie-integrations:nessie-spark-extensions-3.5_2.12:0.79.0",
        )
        .config(
            "spark.sql.extensions",
            "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions,"
            "org.projectnessie.spark.extensions.NessieSparkSessionExtensions",
        )
        .config("spark.sql.catalog.nessie", "org.apache.iceberg.spark.SparkCatalog")
        .config("spark.sql.catalog.nessie.catalog-impl", "org.apache.iceberg.nessie.NessieCatalog")
        .config("spark.sql.catalog.nessie.uri", "http://nessie-server:19120/api/v2")
        .config("spark.sql.catalog.nessie.ref", "main")
        .config("spark.sql.catalog.nessie.authentication.type", "BEARER")
        .config("spark.sql.catalog.nessie.authentication.token", "${NESSIE_TOKEN}")
        .config("spark.sql.catalog.nessie.warehouse", "s3://coomia-dip-lakehouse/warehouse")
        .config("spark.sql.catalog.nessie.io-impl", "org.apache.iceberg.aws.s3.S3FileIO")
        .config("spark.sql.catalog.nessie.s3.endpoint", "http://minio:9000")
        .getOrCreate()
    )
    return spark

#4.2 Nessie SQL Extensions

SQL
-- Create a branch
CREATE BRANCH dev IN nessie FROM main;

-- Switch to a branch
USE REFERENCE dev IN nessie;

-- Create a table on the branch
CREATE TABLE nessie.ontology_db.ontology_objects (
    tenant_id    STRING,
    object_rid   STRING,
    object_type  STRING,
    display_name STRING,
    properties   STRING,
    created_at   TIMESTAMP,
    updated_at   TIMESTAMP
) USING iceberg
PARTITIONED BY (tenant_id, days(created_at));

-- View change log
SHOW LOG IN nessie;

-- Merge branches
MERGE BRANCH dev INTO main IN nessie;

-- Time-travel query
SELECT * FROM nessie.ontology_db.ontology_objects
VERSION AS OF 'main@1234567890abcdef';

-- Create a tag
CREATE TAG v1_0_release IN nessie AS OF main;

-- List all references
SHOW REFERENCES IN nessie;

-- Diff between two references
SHOW DIFF BETWEEN main AND dev IN nessie;
Java
// data-Layer/flink/NessieIcebergSink.java
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

Map<String, String> catalogProps = new HashMap<>();
catalogProps.put("type", "iceberg");
catalogProps.put("catalog-impl", "org.apache.iceberg.nessie.NessieCatalog");
catalogProps.put("uri", "http://nessie-server:19120/api/v2");
catalogProps.put("ref", "main");
catalogProps.put("warehouse", "s3://coomia-dip-lakehouse/warehouse");

CatalogLoader catalogLoader = CatalogLoader.custom(
    "nessie", catalogProps, new Configuration()
);

FlinkSink.forRowData(dataStream)
    .tableLoader(TableLoader.fromCatalog(catalogLoader, TableIdentifier.of("ontology_db", "events")))
    .overwrite(false)
    .build();

#5. Conflict Resolution Strategies

#5.1 Merge Conflict Types

Conflict TypeDescriptionResolution
Same table, different rowsTwo branches modified different rows of the same tableAuto-merge
Same table, same rowTwo branches modified the same rowManual intervention
Schema conflictTwo branches applied different schema changes to the same tableManual intervention
Table-level conflictOne branch deleted a table that another branch is modifyingManual intervention

#5.2 Conflict Resolution Configuration

Python
# data-Layer/nessie/conflict_resolver.py
class NessieConflictResolver:
    """Merge conflict resolver for Nessie"""

    def merge_with_resolution(
        self,
        from_branch: str,
        to_branch: str,
        resolution_strategy: str = "THEIRS",
    ):
        """Merge with configurable conflict resolution"""
        try:
            self.nessie.merge(
                from_ref=from_branch,
                to_ref=to_branch,
                merge_behavior={
                    "default_merge_type": "NORMAL",
                    "key_merge_types": {
                        "ontology_db.ontology_objects": "FORCE",
                        "ontology_db.audit_log": "DROP",
                    },
                },
            )
        except NessieMergeConflictError as e:
            conflicts = e.conflicts
            for conflict in conflicts:
                print(f"Conflict on key: {conflict.key}")
                print(f"  Source: {conflict.source_operation}")
                print(f"  Target: {conflict.target_operation}")

            if resolution_strategy == "THEIRS":
                self._resolve_theirs(from_branch, to_branch, conflicts)
            elif resolution_strategy == "OURS":
                self._resolve_ours(to_branch, conflicts)
            else:
                raise  # Requires manual intervention

#5.3 Best Practice: Minimize Conflict Surface

The most effective conflict prevention strategies in production:

  1. Short-lived branches: Keep ETL branches alive for minutes, not hours
  2. Append-only writes: Prefer APPEND mode over OVERWRITE for frequently written tables
  3. Partition-level isolation: Design ETL jobs to write to distinct partitions, avoiding row-level conflicts
  4. Sequential merge queues: For critical tables, serialize merges through a queue rather than allowing concurrent merge attempts

#6. Performance Benchmarks and Monitoring

#6.1 Nessie Operation Benchmarks

Operation10 tables100 tables1,000 tables10,000 tables
Create branch2ms3ms5ms12ms
Merge branch15ms45ms180ms820ms
Get commit log (100 entries)8ms8ms8ms8ms
Create tag2ms2ms2ms2ms
Diff (two versions)5ms18ms65ms280ms
List all tables3ms12ms85ms450ms

Key observations:

  • Branch creation is nearly constant-time because it only creates a pointer
  • Merge cost scales linearly with the number of changed tables, not total tables
  • Commit log retrieval is independent of catalog size (hash-chain traversal)

#6.2 Monitoring Configuration

YAML
# deployment-Layer/monitoring/prometheus/nessie-metrics.yml
- job_name: 'nessie'
  scrape_interval: 15s
  metrics_path: '/q/metrics'
  static_configs:
    - targets: ['nessie-server:19120']

Critical monitoring metrics:

MetricAlert ThresholdDescription
nessie_version_store_commit_countTotal commit count
nessie_version_store_merge_duration_ms> 5000Merge operation latency
nessie_version_store_branch_count> 100Active branch count
nessie_api_request_duration_ms_p99> 1000API P99 latency
nessie_gc_expired_contents_countGC cleaned content count

#6.3 Alerting Rules

YAML
# deployment-Layer/monitoring/prometheus/nessie-alerts.yml
groups:
  - name: nessie-alerts
    rules:
      - alert: NessieMergeSlow
        expr: nessie_version_store_merge_duration_ms_p99 > 5000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Nessie merge operations are slow"
          description: "P99 merge latency exceeds 5 seconds"

      - alert: NessieTooManyBranches
        expr: nessie_version_store_branch_count > 100
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Too many active Nessie branches"
          description: "Consider running branch cleanup"

#7. Comparison with Palantir Foundry

CapabilityPalantir Foundrycoomia-dip (Nessie)
Data version controlTransaction logGit-like branch/merge
Environment isolationLimitedFull branch isolation
Rollback mechanismDataset-level rollbackArbitrary granularity rollback
Schema evolutionOnline changesBranch-validate-then-merge
Audit trailYesComplete commit history
Time travelLimitedAny historical version
Multi-engine integrationProprietaryOpen Iceberg standard

The key architectural difference: Foundry's versioning is tightly coupled to its proprietary dataset format, while coomia-dip leverages open standards (Nessie + Iceberg) that work with any engine in the ecosystem.

#8. Common Pitfalls and Solutions

#8.1 Branch Proliferation Causing Performance Degradation

Problem: Automated pipelines create branches but fail to clean them up, leading to thousands of dangling references.

Solution:

Python
def cleanup_stale_branches(nessie_client, max_age_days=7):
    """Periodically clean up merged and stale branches"""
    refs = nessie_client.list_references()
    cutoff = datetime.now() - timedelta(days=max_age_days)
    protected = {"main", "dev", "staging"}

    for ref in refs:
        if ref.type == "BRANCH" and ref.name not in protected:
            last_commit = nessie_client.get_log(ref.name, limit=1)
            if last_commit[0].commit_meta.commit_time < cutoff:
                nessie_client.delete_reference(ref.name)

#8.2 Storage Bloat from Uncollected Snapshots

Problem: Iceberg snapshot files accumulate without garbage collection.

Solution:

PROPERTIES
# Nessie GC configuration
nessie.gc.default-cutoff-policy=P30D
nessie.gc.new-files-grace-period=PT1H
nessie.gc.schedule=0 0 2 * * ?

#8.3 Frequent Merge Conflicts

Problem: Multiple concurrent ETL jobs modify the same table.

Solution:

  • Use short-lived branches (minute-level lifetime)
  • Prefer APPEND mode over OVERWRITE for high-write tables
  • Design partition-level writes to avoid row-level conflicts
  • Implement a merge queue for critical tables

#8.4 Authentication Token Expiration

Problem: Long-running Spark/Flink jobs fail mid-execution due to expired Nessie tokens.

Solution:

Python
class NessieTokenRefresher:
    """Auto-refresh Nessie authentication tokens"""

    def __init__(self, token_endpoint: str, client_id: str, client_secret: str):
        self.token_endpoint = token_endpoint
        self.client_id = client_id
        self.client_secret = client_secret
        self._token = None
        self._expiry = 0

    def get_token(self) -> str:
        if time.time() > self._expiry - 60:  # Refresh 60s before expiry
            self._refresh()
        return self._token

    def _refresh(self):
        resp = requests.post(self.token_endpoint, data={
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
        })
        data = resp.json()
        self._token = data["access_token"]
        self._expiry = time.time() + data["expires_in"]

#Key Takeaways

  1. Data version control is a necessity, not a luxury: In coomia-dip, Nessie elevates data change management to the same level as source code version control. Atomic ETL guarantees and zero-risk rollback significantly reduce operational risk.

  2. The branch model is the best approach for environment isolation: Compared to traditional database replication or snapshot approaches, Nessie's branching provides zero-storage-overhead environment isolation. Developers can experiment freely on their own branches without affecting production data.

  3. Deep Iceberg integration is the key differentiator: Nessie's value lies in its native integration with the Iceberg table format. Through the Nessie Catalog, any Iceberg-compatible engine (Spark, Flink, Trino, Doris) automatically gains version control capabilities without any code changes.

#Next Article

S8-04: Apache Iceberg in Practice: Table Format Evolution and Time Travel — A deep dive into Iceberg's internal mechanisms, including the metadata hierarchy, snapshot management, partition evolution, and advanced usage scenarios when combined with Nessie.

Tags: #apache-nessie #version-control #git-for-data #lakehouse #iceberg #data-branching #coomia-dip #Layer-c