Partition and Sort Keys on DynamoDB: Modeling data for batch-and-stream convergence

The Delivery of the Keys to Saint Peter — Pietro Perugino

How to design partition keys, sort keys and a serving layer when your data has two sources with different SLAs

The Delivery of the Keys to Saint Peter — Pietro Perugino

Batch pipelines are reliable, auditable, and built to handle complexity at scale, but they come with a cost: by the time data is consolidated, validated, and ready to serve, hours or days might have passed. For internal analytics that’s often fine, but for a customer staring at a mobile app wondering whether their action was registered, it is not.

This article is about a DynamoDB modeling problem that lives at that boundary. The case study below is fictional and designed to keep the focus on the technical problem rather than on any specific domain. Business rules and delays are simplified accordingly.

The core challenge is one that appears frequently in data-intensive systems: a batch pipeline owns the authoritative view of the data, but its consolidation cycle introduces a lag that makes the user-facing experience feel stale. The goal is to bridge that gap with a near-real-time layer without replacing the batch, without duplicating business logic, and without turning the serving layer into a consistency problem.

The Use Case

I’ve covered this use case in my previous articles about DynamoDB. Here’s a quick recap:

We’re a financial institution developing a new feature in our mobile app to promote our customers’ financial health, offering them monthly saving goals. As a reward for reaching these goals, the saved amount is automatically invested under special conditions and higher interest rates.

The goals are calculated in batch by a data platform and then loaded into the database to be displayed in the app. The only information the app writes to the database is which goal the customer chose.

For this article, we’re extending the use case. The batch pipeline remains the source of truth, but business needs a new capability: when a customer makes a deposit toward their goal, it should appear in the app within minutes, not the next day.

Each customer has a monthly saving goal per investment type: savings account, government bonds and a certificate of deposit. Each goal has a target amount and accumulates progress as the customer makes deposits over the course of the month. The batch pipeline computes the consolidated state of each goal daily and writes it to DynamoDB.

The catch is that a contribution is not immediately final; it goes through reconciliation across multiple systems before it can be considered confirmed. The new capability is about making contributions visible before the batch catches up and display them with a status of “pending confirmation.” If reconciliation fails, it disappears. If it succeeds, the batch pipeline will eventually pick it up and reflect it in the authoritative view. This gives us two data sources with different freshness and different levels of trust, both feeding the same API response. That is the problem this article addresses.

Solution diagram

The diagram below shows how the two pipelines are organized and where they converge.

The batch pipeline reads from consolidated gold tables stored as files on an object storage, S3 in this case, and a Spark-based job applies business rules to compute the updated state for each customer. Those rules live in a relational database, shared between both pipelines. Once processed, the results are bulk loaded into DynamoDB.

The stream pipeline consumes customer activity events from a managed Kafka cluster and a containerized consumer service enriches each event before writing to a second DynamoDB table in near real-time. The two pipelines never write to the same table. They converge only at read time, where a single service merges both into one API response.

This is a practical application of the lambda architecture pattern, but what matters here is what that pattern demands from the data modeling and serving layer, and that is what the next section addresses.

Why two tables

The instinct when modeling in DynamoDB is to reach for the single-table design. In this case, two reasons pull the tables apart.

The first is TTL. DynamoDB’s time-to-live is a table-level configuration, not item-level behavior you can selectively enable. The NRT table needs items to expire automatically after a few days, while the batch table does not; its lifecycle is controlled by the batch job itself, which rewrites the data on each run. Sharing a table would mean either applying TTL to records that should never expire or managing expiration manually through application logic.

The second is the write pattern. The batch pipeline performs a bulk load on a daily schedule, replacing all existing records. The NRT consumer writes continuously, one event at a time, with idempotency requirements that bulk loads do not need. These are different operational profiles, and mixing them in a single table introduces coupling between two pipelines that can be independent. It also sounds like a nightmare for scenarios where reprocessing might be needed.

Splitting the tables keeps each pipeline simple, isolated and easier to operate. The complexity of merging them is contained in the serving layer.

Modeling the tables

A DynamoDB table does not have a fixed schema in the traditional sense, since each item can have different attributes, but every item must have a primary key. That key can be a single attribute, the partition key, or a composite of two, the partition key plus a sort key. DynamoDB uses the partition key to distribute data across storage nodes, and when a sort key is present, it orders items within the same partition.

*The choice of key matters more than anything else in DynamoDB modeling, because it determines how data is distributed, how it is accessed and what queries are possible.
*

The batch table

The batch table holds the consolidated view of each customer: personal attributes, preferences, the goals they have chosen, the progress computed by the last batch run, and a few other fields that the API exposes to the mobile app. It is, effectively, the full customer profile + information about the goals the customer has already accomplished.

Every read starts with a customer. The API asks “what does this person look like right now?” and expects one consolidated response. Given that access pattern, there is no reason to spread the data across multiple items. The partition key is personId.

{
  "personId": "person-uuid",
  "name": "Ana Silva",
  "preferences": { "notifications": true, "language": "pt-BR" },
  "goals": [
    {
      "goalId": "SAVINGS_ACCOUNT",
      "targetAmount": 500.00,
      "savedAmount": 320.00,
      "status": "IN_PROGRESS",
      "specialRate": 0.08
    },
    {
      "goalId": "GOVERNMENT_BONDS",
      "targetAmount": 1500.00,
      "savedAmount": 0.00,
      "status": "NOT_STARTED",
      "specialRate": 0.12
    },
    {
      "goalId": "CERTIFICATE_OF_DEPOSIT",
      "targetAmount": 2000.00,
      "savedAmount": 2000.00,
      "status": "COMPLETED",
      "specialRate": 0.14
    }
  ],
  "lastUpdated": "2026-05-15T03:00:00Z"
}

There is no TTL on this table because nothing needs to expire passively between runs. The batch job owns the lifecycle of every item, rewriting records on each run.

The NRT table

The NRT table has a narrower scope. It holds only what the stream pipeline produces: the most recent balance update per investment type per customer, written as deposit events arrive. There is no need to model the rest of the customer here, because the serving layer will combine this data with the batch table at read time.

The partition key is personId and the sort key is goalId. Every Kafka event carries the customer’s updated balance for one investment type, and the consumer writes it to the NRT table using PutItem. Because there can be at most one pending balance per investment type at any moment, newer events naturally overwrite older ones for the same (personId, goalId) pair. The serving layer can retrieve all pending balances for a customer in a single query.

{
  "personId": "person-uuid",
  "goalId": "SAVINGS_ACCOUNT",
  "currentBalance": 370.00,
  "status": "PENDING_CONFIRMATION",
  "eventId": "event-uuid-123",
  "updatedAt": "2026-05-16T14:23:00Z",
  "ttl": 1747699380
}

The ttl attribute is a Unix timestamp telling DynamoDB when to delete this item automatically. That is how time-to-live works in DynamoDB: you designate an attribute to hold the expiration timestamp, enable TTL on the table pointing to that attribute and Dynamo handles deletion in the background. Items in the NRT table expire after five days, long enough for the batch pipeline to process the contribution and take ownership, short enough to prevent stale pending records from accumulating unnecessarily.

The updatedAt attribute carries the event timestamp from Kafka and plays a role in the merge logic: the serving layer compares it against the batch’s lastUpdated attribute to decide whether the NRT snapshot is fresher than the batch view. If the batch has already caught up, the NRT value is ignored; otherwise it is used and the goal is displayed with a “pending confirmation” indicator.

Serving the data

The two tables exist to be merged. Every read from the API does the same thing: fetch the customer’s batch view, fetch any pending updates from the NRT table and combine them into a single response before returning it to the app.

The two reads are independent and can run in parallel. The first is a GetItem on the batch table, keyed by personId. It returns the customer profile and the consolidated state of every goal as of the last batch run. The second is a Query on the NRT table, also keyed by personId. It returns every pending balance update for that customer across all investment types, typically zero to three items.

batch_response = dynamodb.get_item(
    TableName="CustomerBatch",
    Key={"personId": person_id}
)

nrt_response = dynamodb.query(
    TableName="CustomerNRT",
    KeyConditionExpression="personId = :pid",
    ExpressionAttributeValues={":pid": person_id}
)

batch_item = batch_response.get("Item", {})
pending_by_goal = {
    item["goalId"]: item
    for item in nrt_response.get("Items", [])
}

response_goals = []
for goal in batch_item.get("goals", []):
    pending = pending_by_goal.get(goal["goalId"])
    if pending and pending["updatedAt"] > batch_item["lastUpdated"]:
        response_goals.append({
            **goal,
            "savedAmount": pending["currentBalance"],
            "status": "PENDING_CONFIRMATION"
        })
    else:
        response_goals.append(goal)

A subtle but important property of this design is that the API does not need to know which events have already been incorporated by the batch, nor does it need to track any history of pending updates. The timestamp comparison is enough and the TTL eventually removes the NRT item.

About the costs

The GetItem on the batch table consumes 1 RCU for a strongly consistent read or 0.5 RCU for an eventually consistent one on items under 4KB.

The Query on the NRT table consumes RCUs proportional to the total size of the items returned, rounded up to the nearest 4KB block. With at most 3 items per customer (one per investment type) and small payloads, this almost always lands within a single 4KB block, meaning 1 RCU strongly consistent or 0.5 RCU eventually consistent.

For this use case, eventually consistent reads are acceptable on both tables. The batch item is small enough to fit in a single read unit and the NRT query returns at most 3 small items per customer, so the total read cost per API request lands at roughly 1 RCU. This is a direct consequence of the modeling choices: bounded goal count, single-item batch view, one NRT item per investment type.

Closing thoughts

What makes this work in DynamoDB is the modeling, and most of it comes down to how you use partition keys and sort keys. They are not just identifiers, they are the access path. The partition key decides how data is distributed and which queries are cheap. The sort key decides how items are organized within a partition and which queries are even possible.

Sort keys can do much more than what is depicted here. They support range queries with operators like begins_with, between and inequality comparisons, which means you can model hierarchies, timelines or status transitions directly in the key. Composite sort keys like STATUS#ACTIVE#DATE#2026-05-16 let a single Query retrieve, sort and filter items in ways that would otherwise require a secondary index or a scan.

If you are working on something similar or have approached the same problem differently, I would love to hear about it.

Auto-Generate Optimized GitHub Actions Workflows For Any Stack With This New CLI Tool

We’ve all been there.

You finish building a new project. It’s clean, it’s working, and it’s ready to ship.

But then comes the worst part: Writing the CI/CD pipeline.

You open Google, copy a 3-year-old GitHub Actions workflow from Stack Overflow, spend 45 minutes debugging YAML indentation errors, and realize you forgot to configure dependency caching or security scans.

I got tired of repeating this manual process, so I built a zero-config CLI tool to solve it once and for all: ciraft.

🔍 What is ciraft?

ciraft is a lightweight CLI utility that scans your codebase, auto-detects your programming languages, package managers, and container setups, and instantly generates a production-grade CI/CD pipeline in seconds.

Instead of writing YAML from scratch, you just run:

npx ciraft

🛠️ How It Works (Behind the Scenes)

When you run ciraft, it performs a local analysis of your project files:

Detects your package managers: npm, pnpm, yarn, pip, poetry, go, or cargo.
Scans for tests and linters: Jest, pytest, vitest, ruff, etc.
Looks for Docker configuration: Dockerfiles, multi-stage targets, and Compose configurations.
Applies security best practices: Sets strict permissions, sets up matrix builds, configures concurrency gates, and automatically appends a Trivy vulnerability scan for Docker images.
It then renders a optimized .github/workflows/ci.yml using pre-compiled templates.

⚡ Try It Right Now
You can run it via npx or install it directly using Homebrew:

bash

Install via Homebrew

brew install https://raw.githubusercontent.com/DhanushNehru/ciraft/main/Formula/ciraft.rb

# Scan and generate
ciraft

🤝 Let’s Collaborate (PRs Welcome!)

ciraft is 100% open-source, and I want to support every stack under the sun.

Currently, it supports Node.js, TypeScript, Python, Go, Rust, and Docker.

If you want to add support for a new language (like PHP, Java, Ruby, or Elixir) or a new CI provider (like GitLab CI or Jenkins), check out the repository! I have pre-written 30+ detailed GitHub Issues tagged with good first issue and hacktoberfest to help you get started.

👉 Check out the repository, drop a ⭐ if it saves you time, and let’s build this together: ciraft

💬 What’s the most annoying part of writing CI/CD pipelines for you? Let’s discuss in the comments below! 👇

Unchaining the African Creator Economy

The Problem We Were Actually Solving

We were tasked with creating a platform that would allow creators to sell digital products directly to their audience, bypassing traditional payment gateways that didn’t support African markets. PayPal, Stripe, Gumroad, and Payhip – the usual suspects – were all out of the question due to local restrictions and high fees. Our creators needed a solution that was both scalable and cost-effective.

We knew that building a custom payment system from scratch was a non-starter, given the sheer number of integrations and regulatory compliance requirements. Our only option was to choose a suitable platform that could support our unique needs.

What We Tried First (And Why It Failed)

Our initial approach was to use a popular headless e-commerce platform that promised flexibility and customization. We spent weeks integrating it with our existing infrastructure, only to discover that it was woefully inadequate for our use case. The platform’s payment processing fees were astronomical, and the transactional API was a black box that we couldn’t fully instrument. Our creators were pricing themselves out of the market, and we were hemorrhaging money on transaction fees.

What’s more, the platform’s documentation was a joke, and the community support was non-existent. We were on our own, trying to troubleshoot issues that we didn’t have the expertise to handle. It was a recipe for disaster.

The Architecture Decision

After months of searching, we finally found a suitable platform that checked all the right boxes. Mollie, a Dutch payment service provider, offered a fully-fledged payment gateway that supported multiple currencies, including the local Nigerian naira. Their API was robust, well-documented, and easy to integrate. We were able to hook up our creator’s products to Mollie’s payment flow with ease, and the fees were a fraction of what we were paying before.

But here’s the thing: Mollie wasn’t a traditional payment gateway. They didn’t have a sprawling front-end that we needed to integrate with. Instead, they provided a comprehensive backend API that we could use to create our own checkout flow. This requirement added a level of complexity to our platform, requiring us to build out a custom checkout experience from scratch.

What The Numbers Said After

After deploying Mollie to production, our creators saw a significant increase in sales and revenue. Our transaction fees plummeted, from 5.5% to 2.9%. But more importantly, our creators were now able to price their products competitively, knowing that they had a reliable payment partner.

According to our analytics, our creator’s average order value (AOV) increased by 25% after integrating with Mollie. This was a clear indication that our creators were finally able to capture value at the point of sale, thanks to Mollie’s reliable payment processing.

What I Would Do Differently

If I were to do it all over again, I’d make sure to involve our creators in the platform decision-making process from the get-go. We learned too late that they had specific pain points and requirements that we didn’t account for initially. By involving them in the decision-making process, we could have avoided some of the initial integration issues and gotten them on board with our platform design from day one.

Secondly, I’d invest more in API documentation and instrumentation. While Mollie’s API was excellent, we had to spend a significant amount of time instrumenting their API calls and monitoring our transaction flows. With better documentation and instrumentation, we could have avoided some of the issues we faced during the integration process.

The Treasure Hunt Engine Gotcha – A Lesson in Constrained Performance

The Problem We Were Actually Solving

When we first set out to optimize the Treasure Hunt Engine, our primary goal was to improve player engagement. We wanted to make the treasure hunt experience more challenging, rewarding, and generally more fun. We spent countless hours tweaking algorithms, experimenting with different game logic, and fine-tuning the UI. But as we delved deeper into the code, we realized that our changes were having an unintended consequence: the engine was becoming increasingly CPU-intensive.

What We Tried First (And Why It Failed)

Initially, we focused on optimizing the game logic, assuming that the engine’s performance was directly tied to the complexity of the rules. We implemented various caching mechanisms, parallelized expensive computations, and even rewrote the entire engine using a more efficient language. However, our efforts were largely ineffective, and the engine’s performance continued to suffer. We were baffled – why were our optimizations not having the desired effect?

The problem was that we were optimizing the wrong thing. The game logic was indeed complex, but the true bottleneck was the way we were loading the game world data into memory. The engine was creating a massive, densely packed graph of objects, which was causing the CPU to grind to a halt. We were treating the symptoms, not the cause.

The Architecture Decision

Around this time, we decided to revisit our architecture and make a fundamental change: we would offload the game world data to a separate process, using a message-passing interface to communicate between the engine and the data store. This would allow us to scale the data store independently of the engine, reducing the memory footprint and CPU load. We also introduced a caching layer to minimize the number of requests made to the data store.

What The Numbers Said After

After implementing the new architecture, we ran a series of benchmarking tests to measure the engine’s performance. The results were striking: our CPU usage had dropped by 30%, and the engine was now able to handle 50% more players without a noticeable increase in latency. The caching layer was also a huge success, reducing the number of requests made to the data store by 75%. We had finally broken through the performance bottleneck.

What I Would Do Differently

Looking back, I wish we had taken a more systematic approach to performance optimization. Instead of trying to optimize the game logic, we should have started by profiling the engine’s memory usage and identifying the source of the performance issue. We also should have considered the potential trade-offs of our architecture changes, such as the increased complexity of the message-passing interface.

In the end, our experience with the Treasure Hunt Engine taught us a valuable lesson: when optimizing performance, it’s essential to identify the root cause of the problem, rather than treating the symptoms. By taking a more careful and systematic approach, we can avoid the common pitfalls of premature optimization and achieve better results in the long run.

great_cto v2.17 – no more tambourine dance

great_cto v2.17 – no more tambourine dance

If you’ve ever spent 20 minutes setting up Claude Code plugins before you could even start working – this update is for you.

One install, everything works.

Previously: install great_cto, then figure out that Superpowers and Beads are also needed, find the repos, clone them, enable them in settings, restart. Classic.

Now – one command:

npx great-cto install

Done. Superpowers and Beads install automatically as companion plugins. They land in ~/.claude/plugins/cache/local/, get enabled in settings.json, and are ready to work. If git is missing – great_cto gives a friendly hint instead of silently failing.

Jurisdiction-aware agents.

The new jurisdictions module detects the context of your project – EU, US, Canada, UK, Australia, and others – and automatically activates the right regulatory reviewer agents.

Working on a fintech product for European users? The EU reviewer turns on automatically. Building for the Canadian market? PIPEDA gets covered. No manual configuration, no trying to remember what applies where.

Eight jurisdictions are currently supported, and the list keeps growing.

llm-leash UI: 16 new features.

The board got a serious upgrade. The most useful ones:

  • Cmd-K – global command palette for navigation.
  • Issues subtab – all security and compliance findings in one place.
  • Session timeline – visual history of what happened and when.
  • Topology graph – shows agent dependencies. Useful when you have 5+ parallel agents.
  • HITL diff – human-in-the-loop review of agent changes before they’re applied.
  • OPA config – Open Policy Agent integration for compliance rules.
  • SOC2 export – one-click audit trail for compliance officers.
  • Rule comparison – compare policy versions side by side.

Companion plugins out of the box.

A bit more detail on how the Superpowers + Beads bundle works, since the architecture is non-obvious.

Superpowers – a methodology plugin. It gives Claude Code skills: /brainstorm, /write-plan, /execute-plan, code review workflow, TDD cycle, parallel agent execution. Without it, Claude acts on vibes. With it – on a structured plan.

Beads – a git-native task tracker. Tasks live as commits, survive session restarts, have dependencies and blockers. Claude creates and closes them autonomously as it works.

great_cto – the orchestration layer. It routes requests to the right agents, enforces reviewers based on archetype and jurisdiction, manages the agent pipeline.

Together: you describe what needs to be done, great_cto breaks it into a plan, Beads tracks it, Superpowers enforces methodology, the right reviewer agents plug in automatically.

TL;DR

npx great-cto install

Feedback and PRs welcome.

When Catalogs Are Embedded in Storage

Standalone catalogs versus embedded catalogs showing how the architecture simplifies

This is Part 8 of a 15-part Apache Iceberg Masterclass. Part 7 covered the traditional catalog landscape. This article examines a newer approach: embedding the catalog directly inside the storage layer.

Traditional Iceberg architectures have three components: the query engine, a standalone catalog, and object storage. Embedded catalogs collapse the catalog into the storage layer itself, reducing the number of services to manage while providing built-in table maintenance.

Table of Contents

  1. What Are Table Formats and Why Were They Needed?
  2. The Metadata Structure of Current Table Formats
  3. Performance and Apache Iceberg’s Metadata
  4. Technical Deep Dive on Partition Evolution
  5. Technical Deep Dive on Hidden Partitioning
  6. Writing to an Apache Iceberg Table
  7. What Are Lakehouse Catalogs?
  8. Embedded Catalogs: S3 Tables and MinIO AI Stor
  9. How Iceberg Table Storage Degrades Over Time
  10. Maintaining Apache Iceberg Tables
  11. Apache Iceberg Metadata Tables
  12. Using Iceberg with Python and MPP Engines
  13. Streaming Data into Apache Iceberg Tables
  14. Hands-On with Iceberg Using Dremio Cloud
  15. Migrating to Apache Iceberg

The Embedded Catalog Model

Standalone catalogs versus embedded catalogs showing how the architecture simplifies

In a traditional setup, a separate catalog service (Polaris, Glue, Nessie) runs alongside object storage. The engine talks to the catalog to get metadata pointers, then reads data from storage. Two services, two sets of credentials, two operational concerns.

In an embedded model, the storage service itself manages Iceberg metadata. When you create a table, the storage system creates the metadata files internally and handles atomic commits, compaction, and snapshot management. The engine interacts with a single endpoint that serves both catalog operations and data access.

AWS S3 Tables

S3 Tables architecture showing the built-in Iceberg catalog with automatic compaction

AWS launched S3 Tables in late 2024 as a new S3 bucket type designed specifically for Iceberg tables. When you create an S3 table bucket, AWS manages the Iceberg catalog internally.

How it works: You create tables through the S3 Tables API or through engines like Athena and EMR. S3 Tables stores the Iceberg metadata alongside the data in the same bucket, handling the catalog pointer, manifest management, and atomic commits behind the scenes.

Built-in maintenance: S3 Tables runs automatic compaction in the background, merging small files into optimally-sized ones without any user configuration. It also handles snapshot expiry and orphan file cleanup. This eliminates one of the biggest operational burdens of Iceberg (covered in Part 10).

Access via REST API: S3 Tables exposes tables through a REST-catalog-compatible interface. Dremio, Spark, Trino, and other engines that support the Iceberg REST catalog can connect to S3 Tables directly.

Built-in lifecycle management: Beyond compaction, S3 Tables handles the entire table maintenance lifecycle. Snapshot expiry happens automatically based on configurable retention policies. Orphan files are cleaned up without user intervention. For teams that do not want to manage maintenance schedules, this is a significant operational advantage.

Limitations: S3 Tables is AWS-only. Tables are stored exclusively in S3 and cannot be moved to other cloud providers without migration. Cross-engine governance is limited to what AWS IAM provides. If you need fine-grained access control beyond IAM policies (column-level masking, row-level filters), you need a standalone catalog layer on top.

Cost model: S3 Tables uses a different pricing model than standard S3. Storage and request costs are similar, but the built-in maintenance operations (compaction, expiry) are included in the service price. Compare this to running Spark compaction jobs on EMR, which adds compute costs on top of storage.

Table bucket vs. general-purpose bucket: S3 Tables uses a new “table bucket” type, separate from standard S3 buckets. You cannot mix table data with other objects in a table bucket, and standard S3 operations (ls, cp, rm) do not work on table bucket contents. All interaction goes through the S3 Tables API or through Iceberg-compatible engines.

MinIO AI Stor

MinIO AI Stor takes a similar approach for on-premises and private cloud deployments. MinIO, the leading S3-compatible object storage system, embeds Iceberg catalog functionality directly into the storage layer.

How it works: MinIO manages Iceberg table metadata as part of its storage operations. When data is written, MinIO handles the catalog updates, file tracking, and maintenance internally.

Key differentiator: MinIO is designed for on-premises deployments and private clouds, making it the embedded catalog option for organizations that cannot use public cloud services. It also integrates vector storage capabilities for AI workloads alongside Iceberg tables.

S3 compatibility: Because MinIO implements the S3 API, engines that work with S3 (Spark, Trino, Dremio) can interact with MinIO-managed Iceberg tables with minimal configuration changes. This makes it a drop-in replacement for S3 in on-premises environments.

GPU-accelerated analytics: MinIO AI Stor integrates with GPU-aware processing frameworks, enabling direct analytics on Iceberg data without moving it to a separate compute layer. This is relevant for organizations running AI/ML workloads alongside traditional analytics.

When Embedded Catalogs Make Sense

Decision tree for choosing between embedded and standalone catalogs

Scenario Recommendation
AWS-only, want minimal ops S3 Tables
On-premises, private cloud MinIO AI Stor
Multi-cloud portability needed Standalone catalog (Dremio Open Catalog)
Cross-engine governance needed Standalone catalog (Polaris)
Multiple storage systems Standalone catalog
Single storage, simple setup Embedded catalog

Embedded catalogs are the right choice when you have a single storage system and want to minimize operational complexity. They trade flexibility for simplicity.

Standalone catalogs remain the better choice when you need multi-cloud support, cross-engine governance, or the ability to query data across multiple storage systems through federation.

The Hybrid Approach

Many organizations use both. An embedded catalog handles the storage-managed tables (S3 Tables for their AWS data), while a standalone catalog like Dremio Open Catalog provides a unified view across all data sources. Dremio can connect to S3 Tables, AWS Glue tables, and standalone catalog tables simultaneously, presenting them all through a single semantic layer.

This hybrid approach lets you pick the simplest catalog for each use case while maintaining a unified analytics experience.

Operational Planning for Embedded Catalogs

When adopting an embedded catalog, plan for these considerations:

Vendor dependency: An embedded catalog ties your tables to the storage vendor’s lifecycle. If the vendor changes pricing, deprecates features, or discontinues the product, migrating away requires converting all tables to a different catalog. With a standalone catalog, switching storage providers only requires changing the storage configuration.

Monitoring limitations: Embedded catalogs provide limited visibility into their internal maintenance operations. You cannot inspect the compaction schedule, tune the target file size, or monitor orphan cleanup progress as precisely as you can with manual maintenance via Spark procedures.

Cross-region access: Embedded catalogs are scoped to a storage region. If your analytics workloads run in a different region than your storage, the embedded catalog adds cross-region latency. A standalone catalog can be deployed in the same region as your compute for lower latency.

Integration testing: Before committing to an embedded catalog for production, test your full query stack (dashboards, notebooks, scheduled pipelines) against the embedded catalog endpoint. Verify that your engines handle the catalog’s REST API implementation correctly, as there can be subtle differences between implementations.

Part 9 covers how table storage degrades over time and why maintenance matters regardless of which catalog you use.

Books to Go Deeper

Free Resources

SafeMind AI: Instant Health & Safety Intelligence

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

The Problem It Solves: During health or safety emergencies, people often face panic, confusion, and language barriers, leading to delayed decision-making. Access to immediate, clear medical guidance or rapid SOS capabilities is critical but often scattered across different apps or blocked by slow response times.

The Experience It Creates: SafeMind AI serves as an all-in-one digital guardian. It provides users with a calm, instantly accessible dashboard that centralizes critical safety tools. The experience is designed to be extremely fast, secure, and easy to use under stress—giving users peace of mind that intelligent help is just a click away.

Core Functions (In Short)
1.AI Health Assistant: A chat interface powered by Google Gemma 4 that instantly analyzes symptoms and provides extremely brief, actionable first-aid bullet points and urgent care advice.
2.Emergency SOS System: A one-tap rapid-response button that visually activates emergency protocols and simulates notifying authorities and emergency contacts.
3.Live Location Tracker: A real-time geolocation map that pinpoints the user’s coordinates and maps out nearby medical and emergency facilities.
4.Community Incident Reports: A logged feed where users can report, view, and track local safety hazards (like road blockages or fires) to maintain situational awareness.
5.Real-time Translation: A communication tool designed to break down language barriers during emergencies, ensuring users can get help no matter where they are.

Demo

https://youtu.be/BHoQZhcPzXw?si=Lqtj0_FmDrfzZ3ZG

Code

https://github.com/V-2007-P/personal-health-assistant.git

How I Used Gemma 4

Gemma 4 acts as the platform’s core medical reasoning engine. When users input emergency symptoms, Gemma 4 instantly analyzes the text and outputs highly-formatted, concise first-aid bullet points and urgent care advice—saving critical seconds when panic sets in.

Model We Chose & Why We utilize a dual-model approach:

1.31B Dense (Cloud/API): Used as our primary model because complex symptom analysis requires deep reasoning and high accuracy to prevent medical hallucinations and strictly adhere to our brief formatting rules.

2.E4B (Local Fallback): Integrated for offline execution. Emergencies often happen without internet access, and the highly-efficient E4B model allows users to run the AI entirely on their local device without sacrificing speed.