Unit Testing vs System Testing: Key Differences, Use Cases, and Best Practices for 2026

As 72% of software development teams now rely on AI code assistants (2026 Stack Overflow Developer Survey), validating code quality across multiple testing levels is more critical than ever. A 2025 IBM study found that fixing a bug found in production costs 15x more than fixing the same bug caught during unit testing, and 5x more than one caught during system testing.

If you have ever written a function that worked perfectly in isolation, only to break an entire user flow when deployed, you have seen the gap between unit testing and system testing firsthand. In this guide, we will break down the core differences between these two foundational testing levels, their use cases, best practices, and how they fit into a modern software delivery pipeline.

Table of Contents

  1. What is Unit Testing?
  2. What is System Testing?
  3. Key Differences: Unit vs System Testing (Side-by-Side Comparison)
  4. Core Similarities
  5. How They Fit in the Testing Pyramid
  6. Top Tools for Each Testing Level
  7. 2026 Best Practices for Teams
  8. FAQs
  9. Final Thoughts

1. What is Unit Testing?

Definition

Unit testing is the lowest level of software testing, focused on validating individual units or components of an application in complete isolation. A unit is the smallest testable part of a codebase, typically a single function, method, or class. Developers write and run unit tests during active development, often before merging code to a shared repository.

Key Characteristics

  • Isolation: Tests run without relying on external dependencies (databases, APIs, third-party services)
  • Blazing fast execution: Most unit tests run in milliseconds, making them suitable for running on every code commit
  • Heavily automated: Rarely run manually, integrated directly into CI/CD pipelines
  • Relies on mocking/stubbing: Uses tools to simulate dependencies so tests only validate the unit itself
  • Narrow scope: Targets a single specific functionality of one component

Who Performs It

Software developers write, run, and maintain unit tests as part of their day-to-day coding workflow.

Common Tools

  • Java: JUnit 5
  • Python: pytest, nose2
  • C++: Google Test
  • JavaScript/TypeScript: Mocha, Jest, Vitest
  • .NET: NUnit, xUnit

Example Unit Test (Python + pytest)

This test validates a function that calculates order totals, with a mocked shipping API dependency:

# Arrange
from unittest.mock import Mock
import pytest
from order import calculate_total

def test_calculate_total_with_discount():
    # Mock external shipping API to avoid real calls
    mock_shipping_api = Mock()
    mock_shipping_api.calculate.return_value = 5.99

    # Act
    total = calculate_total(
        item_price=29.99,
        discount=10,
        shipping_client=mock_shipping_api
    )

    # Assert
    assert total == 32.98 # 29.99 * 0.9 + 5.99
    mock_shipping_api.calculate.assert_called_once()

Best Practices

  • Follow the Arrange-Act-Assert pattern for all tests
  • Test one specific behavior per test case
  • Keep tests independent (no shared state between tests)
  • Use descriptive test names (e.g., test_calculate_total_applies_10_percent_discount instead of test_total_1)
  • Mock all external dependencies
  • Run tests on every pull request to catch regressions early

Common Mistakes

  • Testing multiple functionalities in a single test
  • Forgetting to mock external dependencies, leading to slow, flaky tests
  • Hardcoding test values instead of using constants or fixtures
  • Skipping edge case testing (e.g., zero values, negative inputs, maximum limits)
  • Over-relying on AI-generated unit tests without validating they cover intended behavior

2. What is System Testing?

Definition

System testing is a high-level black-box testing technique that evaluates the complete, integrated software system against formal business requirements. It tests the application as a single cohesive unit to verify that all components work together correctly for end users.

Key Characteristics

  • End-to-end scope: Tests the full integrated stack, including frontend, backend, databases, APIs, and third-party integrations
  • Production-like environment: Runs in a staging environment that mirrors production infrastructure as closely as possible
  • Covers functional and non-functional requirements: Validates both that features work as intended, and that they meet performance, security, and usability standards
  • Uses real-world user scenarios: Tests are based on actual user journeys, not internal code logic
  • No knowledge of internal code required: Testers only need to understand expected behavior, not how the code is written

Common Types of System Testing

Type Purpose
Functional Testing Verifies all system features match written requirements
Non-Functional Testing Includes performance, load, stress, accessibility, and usability testing
Recovery Testing Validates the system can recover from crashes, network outages, or data loss
Security Testing Checks for vulnerabilities, unauthorized access, and compliance with regulations like GDPR or PCI DSS

Who Performs It

Dedicated QA engineers, testing specialists, or SDETs (Software Development Engineers in Test) design, run, and maintain system tests.

Example System Test Scenario

For an e-commerce platform, a typical system test would validate the full checkout flow:

  1. User creates an account and logs in
  2. User searches for a product and adds it to their cart
  3. User enters shipping and payment details
  4. User submits the order and receives a confirmation email
  5. Order appears in the admin dashboard and inventory is updated

Best Practices

  • Create a formal test plan aligned with business requirements before starting testing
  • Test both positive (expected success) and negative (expected failure) scenarios
  • Document all test cases with clear steps, expected results, and priority levels
  • Include edge case scenarios (e.g., 100+ items in cart, expired payment cards)
  • Automate repetitive, high-priority flows to reduce manual testing effort

Common Mistakes

  • Starting testing without clear, agreed-upon requirements
  • Skipping non-functional testing, leading to performance outages in production
  • Testing in an environment that does not match production (e.g., smaller server sizes, different database versions)
  • Forgetting to test AI-powered features (e.g., recommendation engines, chatbots) for unexpected behavior
  • Not documenting test results, making it hard to track recurring issues

3. Key Differences: Unit vs System Testing (Side-by-Side Comparison)

The table below summarizes the core differences between the two testing levels:

Aspect Unit Testing System Testing
Scope Individual units/classes/functions Complete integrated end-to-end system
Timing Run during development, on every code commit Run after all components are integrated, before release
Owner Software developers QA engineers, testers, SDETs
Environment Isolated, artificial environment with mocked dependencies Production-like staging environment with real dependencies
Execution Speed Very fast (milliseconds per test) Slower (seconds to minutes per test)
Cost of Fixing Bugs Low (average $100 per bug, IBM 2025) High (average $1,500 per bug, IBM 2025)
Dependency Handling Uses mocks, stubs, and fakes for all external dependencies Uses real databases, APIs, and third-party services
Bug Detection Focus Bugs in individual component logic System-wide integration issues, requirement gaps, performance issues
Coverage Metric Statement/branch/decision coverage Business requirement coverage
Test Type White-box (tester has access to internal code) Black-box (tester does not need access to code)
Documentation Code comments, inline test descriptions Formal test plans, test case repositories, execution reports

4. Core Similarities

While they serve very different purposes, unit and system testing share key foundational traits:

  1. Both are mandatory testing levels in the standard Software Development Lifecycle (SDLC)
  2. Both have the core goal of delivering high-quality, defect-free software to end users
  3. Both can (and should) be automated for efficiency
  4. Both require formal test planning and prioritization to be effective
  5. Both identify defects that would otherwise cause issues for users in production

5. How They Fit in the Testing Pyramid

The testing pyramid is a widely adopted framework for balancing testing efforts to maximize efficiency and coverage:

  • Base (Unit Tests): The largest portion of your test suite (70-80% of all tests). These are the fastest, cheapest, and most reliable tests, designed to catch 80% of bugs early in development.
  • Middle (Integration Tests): Fewer than unit tests (15-20% of all tests), these validate that multiple units work together correctly.
  • Top (System/End-to-End Tests): The smallest portion of your test suite (5-10% of all tests). These are the slowest, most expensive, and most prone to flakiness, so they should only be used to validate critical user journeys.

Common Anti-Pattern to Avoid

The “inverted testing pyramid” (more system tests than unit tests) leads to slow CI/CD pipelines, flaky test suites, and wasted engineering hours fixing tests that could have been avoided with proper unit testing.

6. Top Tools for Each Testing Level

Unit Testing Tools

  • JUnit 5: The de facto standard for Java and JVM-based applications, with support for parameterized tests and nested test classes.
  • pytest: The most popular Python testing framework, known for its simple syntax and robust fixture system.
  • Vitest: Fast, lightweight testing framework for modern JavaScript/TypeScript projects, optimized for Vite and React/Vue/Svelte apps.
  • Google Test: Open-source C++ testing framework with support for test discovery and death tests.

System Testing Tools

  • Cypress 13+: Frontend end-to-end testing tool with native mobile testing support (added 2025) and AI-powered test flakiness detection.
  • Selenium 4: Open-source cross-browser testing tool supporting all major browsers and programming languages.
  • Postman 11: API testing tool with AI-generated test case functionality and native integration with CI/CD pipelines.
  • JMeter 6: Open-source performance and load testing tool for validating system throughput under high traffic.
  • LoadRunner Cloud: Enterprise-grade load testing tool for simulating millions of concurrent users.

7. 2026 Best Practices for Teams

  1. Shift left testing: Integrate unit tests into your PR workflow to catch bugs before they reach QA. Run system tests automatically on every staging deployment.
  2. Align test coverage with business risk: Prioritize unit tests for high-risk modules (payment processing, authentication) and system tests for high-traffic user journeys (checkout, login, signup).
  3. Use AI responsibly: Leverage tools like GitHub Copilot to generate boilerplate unit tests, but always review them to ensure they validate intended behavior. Use AI tools like Testim to generate system test cases for common user flows.
  4. Avoid over-testing: Do not write unit tests for trivial getters/setters, and do not write system tests for edge cases already covered at the unit level.
  5. Unify test reporting: Use tools like Datadog Testing Monitoring to track test pass rates, execution time, and defect escape rates across both unit and system testing levels.

8. FAQs

Q: Can system testing replace unit testing?

A: No. System testing is far slower and more expensive to run and maintain, and it cannot pinpoint the root cause of bugs in individual code components. Catching bugs at the unit level reduces the workload for QA teams and speeds up release cycles.

Q: Do I need to automate both unit and system testing?

A: Yes. 100% of your unit tests should be automated, as they run on every code commit. For system testing, automate all repetitive, high-priority user flows, and reserve manual testing for exploratory testing and edge case scenarios.

Q: How much test coverage do I need?

A: Aim for 70-80% code coverage for core modules with unit tests. For system testing, aim for 100% coverage of critical user journeys, and 80% coverage of secondary flows.

9. Final Thoughts

Unit testing and system testing are not competing practices, they are complementary parts of a robust testing strategy. Unit tests catch bugs early in development, while system tests validate that your entire application works as expected for real users. By following the testing pyramid and shifting testing left, teams can reduce release cycles, cut down on production bugs, and deliver better software to their users.

References

  1. Tutorialspoint: Software Testing Types
  2. Software Testing Help: Unit Testing Guide
  3. ISTQB Certified Tester Foundation Level (CTFL) 2025 Syllabus
  4. 2026 Stack Overflow Developer Survey
  5. IBM 2025 Cost of Software Defects Report

A game design textbook explains why products with fewer features win

Jira owned the project management market. Asana, Monday, Basecamp, Trello, ClickUp all competed on features. Linear’s team walked in with fewer features than any of them and built a following among engineers. Linear is a worse product on paper. It does less. But the engineers who use it will fight you if you try to switch them back.

This pattern repeats across software. Products with fewer features take share in markets where the feature gap has collapsed to zero. The reason comes from game design.

The game is not the experience

Jesse Schell is a former Creative Director at Walt Disney Imagineering, Carnegie Mellon professor, and author of The Art of Game Design: A Book of Lenses (now in its third edition, 113 lenses drawing from psychology, architecture, music, anthropology, and film).

Lens #2: The Lens of Essential Experience. The game is not the experience. The game enables the experience, but it is not the experience. A game designer’s job is to identify the essential experience and design everything to support it. A feature list enables the product, but the user’s experience of it determines whether they stay.

Schell adds the concept of resonance: game themes that touch players do so by fulfilling their fantasies or affirming an idea the player holds to be true. Skyrim’s world is heavily scripted, but the fantasy of total freedom in a simpler world resonated enough that Bethesda sold 60 million copies on it. People buy Stardew Valley over games with 100x its budget because the pitch is the peace of farming minus the labor.

The Lens of Freedom: you don’t need to give the player true freedom, only the feeling of freedom. Notion works even though users rarely touch half its features. The feeling of having a system you built yourself is what keeps them.

Fantasies that won

If the experience runs on fantasy fulfillment, then every product that won a crowded market should have a clear fantasy behind it.

The competence fantasy. Vim and Neovim hold a devoted audience alongside VS Code because the difficulty is the appeal. Mastering Vim feels like beating a boss. The learning curve filters for people who want the fantasy of being an elite engineer. Vim users have a moment where they realize they’re editing text faster than anyone around them. Stack Overflow appeals to the same need: answer a question and the system tells you that you’re an expert. People chose Stack Overflow because it let them prove competence to strangers.

The belonging fantasy. Discord won gaming communities by making “your server” feel like “your guild.” Custom roles and custom emojis made each server feel owned. Slack won workplace communication by making “your workspace” feel like your team’s living room. They serve different markets but both won on the same fantasy: you belong to a group that has its own space.

The control fantasy. Linear’s team is taking Jira’s market by offering the absence of chaos. Calm UI and fast keyboard shortcuts. Engineers pick Linear because it makes them feel like their work is under control, even when it isn’t. Notion appeals to a related need: the workspace is the character sheet, the weekly planner is the quest log. Stripe’s team targets developers with the control fantasy. The API docs and the platform architecture tell the developer that they’re building real infrastructure.

The accumulation fantasy. VS Code’s appeal is power accumulation. Each extension you install and each keybinding you learn makes your editor do more than it did yesterday. GitHub delivers accumulation, and the comparison with GitLab and Bitbucket makes it visible. All three host code. GitHub is the one that turned contribution history into a persistent asset: the green squares, the profile page. GitLab and Bitbucket store your code. GitHub made your contribution history feel like a track record.

The expression fantasy. Figma’s appeal to designers is that the tool for designing things is itself a design game. Designers chose Figma because it let them express and iterate with other people watching. Excalidraw offers expression differently: the hand-drawn aesthetic says “this is a sketch, not a commitment.”

The reward fantasy. Schell’s Lens of Pleasure and Lens of Reward describe why certain game loops keep players coming back: the experience of earning something feels good independent of what you earned. Duolingo’s team built a productivity app around this loop. The streak counter and the XP bar pull you back. The guilt trip from the owl when you miss a day makes sure you don’t leave. Social media runs on the same loop (likes and notifications). Robinhood’s team brought it to investing: confetti on your first trade.

Not every product maps to a clean fantasy (chaos and destruction drive entire game genres but have few product analogues), but the exercise forces you to ask Schell’s question: what is the essential experience?

A lens from outside your discipline

None of the fantasies above came from the engineering discipline. They came from a game design textbook written by a former Disney Imagineer who also worked at IBM as a software engineer.

Grand strategy games train you to think about systems architecture. MMO guilds give you a framework for team dynamics and community products. A game design textbook reframes product-market fit in ways that product management books don’t cover.

The advice to specialize is about depth, and depth matters. But specialization advice usually comes with an implicit “and ignore everything else.” The engineer who reads game design or studies psychology has more mental models to pull from when the feature list runs out and the product needs a reason to exist.

Map your product to the fantasies above. If you can’t articulate which fantasy you offer that your competitors don’t, you’re competing on features, and features will converge over time. Schell’s Lens of Emotion asks: what emotions should the player feel, and are they feeling them? Next time you run a user test, ask what your users felt.

How to Build a Raydium Launchpad Bonding Curve in 5 Minutes with forgekit

If you’ve ever tried building a token launchpad or a bonding curve on Solana, you already know the pain. You spend hours wrestling with complex SDKs, trying to figure out the precise math for a bonding curve, ensuring your token mints are actually “rug-proof,” and praying your Raydium CPMM pool creation doesn’t fail silently.

Most of the tools out there are heavy, monolithic, and full of dependencies you don’t need.

That’s why we built forgekit (@forgekit-labs) – a modular, zero-dependency toolkit that breaks down Solana token launches and Raydium infrastructure into pure, highly-focused functions.

In this tutorial, I’ll show you how to use forgekit to calculate a bonding curve, execute a secure token mint, and graduate a token to a Raydium CPMM pool in under 5 minutes.

What is forgekit?

forgekit is a collection of 8 single-purpose npm packages. Instead of forcing you to install a massive SDK, you only install exactly what you need:

  • @forgekit-labs/curve: Pure bonding curve math (no network required).
  • @forgekit-labs/token: Atomic token minting with built-in authority revocation.
  • @forgekit-labs/launchpad: Raydium CPMM pool creation.
  • @forgekit-labs/meta: Arweave upload via Turbo.
  • …and more for LP splitting, fees, and v0 transaction building.

Let’s build.

Step 1: Install the Modules

For a standard bonding curve launch, you only need three modules. Let’s pull them down:

npm install @forgekit-labs/curve @forgekit-labs/token @forgekit-labs/launchpad

Step 2: Calculate the Bonding Curve

Bonding curves require precise math. Instead of writing custom logic to calculate market caps, progress, and thresholds, we use the pure functions from @forgekit-labs/curve.

Because this package is mathematically pure, it requires zero network calls and executes instantly.

import { calculateGraduationThreshold, getCurveProgress } from '@forgekit-labs/curve';

// Define your curve parameters
const startingPriceLamports = 1000; // Starting price in lamports
const targetSol = 85; // E.g., Graduate the curve when 85 SOL is raised

// Calculate exactly how many tokens need to be sold to hit the threshold
const thresholdTokens = calculateGraduationThreshold(startingPriceLamports, targetSol);

console.log(`To graduate, we need to sell ${thresholdTokens} tokens.`);

Step 3: The “Rug-Proof” Atomic Mint

One of the biggest issues with token launches is ensuring the creator actually revokes the Mint and Freeze authorities. If these aren’t revoked, the token isn’t safe.

@forgekit-labs/token handles this natively. It bundles the minting and authority revocation into a single, atomic transaction. If the revocation fails, the entire mint fails – guaranteeing safety.

import { createAtomicMintTx } from '@forgekit-labs/token';

// Build the transaction
const unsignedTx = await createAtomicMintTx({
  connection,
  creatorPubkey: userWallet.publicKey,
  name: "My Awesome Token",
  symbol: "MAT",
  metadataUri: "https://arweave.net/...", // Use @forgekit-labs/meta to get this!
  decimals: 6,
  totalSupply: 1_000_000_000,
});

// Send this unsigned v0 transaction to the frontend for the user to sign

Step 4: Graduate to Raydium CPMM

Once your curve hits its SOL threshold, it’s time to graduate. The token needs to migrate to a Raydium CPMM liquidity pool. Doing this manually involves massive boilerplate.

With @forgekit-labs/launchpad, it’s a single, idempotent call. It includes built-in validation and fee configuration.

import { createCpmmPool } from '@forgekit-labs/launchpad';

const graduationResult = await createCpmmPool({
  connection,
  baseMint: tokenAddress,
  quoteMint: "So11111111111111111111111111111111111111112", // Wrapped SOL
  baseAmount: thresholdTokens,
  quoteAmount: targetSol,
  // forgekit automatically handles idempotency and validation under the hood
});

console.log(`Graduation successful! Pool ID: ${graduationResult.poolAddress}`);

Conclusion

That’s it. No bloated SDKs, no guessing the math, and no worrying about whether your token authorities were actually revoked.

By splitting these complex actions into isolated, modular packages, @forgekit-labs gives you the exact tools you need to build a premium launchpad or trading terminal, without the headache.

Check out the code and start building:

If you found this useful, drop a star on the repo and let me know what you’re building!

How to turn an AI prototype into a production system

AI tools have changed how fast software gets off the ground. Today, a single developer can go from an idea to a working AI prototype in days, sometimes hours. In a controlled study on GitHub Copilot, developers finished a coding task 55.8% faster with AI help. That’s why prototypes are everywhere right now.

But these tools also hide decisions that production systems cannot afford to ignore. A prototype can “work” while the hard parts stay undefined, like:

  • Who can log in?
  • Where data is allowed to live?
  • Which services exist and how do they talk?
  • What is the deployment model?
  • What does it costs to run?
  • Who owns each part when something breaks?

In many AI-generated prototypes, authentication, data boundaries, infra topology, deployment costs and ownership stay implicit or missing entirely.

AI builders optimize for instant output, but production demands explicit responsibility. If those choices stay hidden, teams hit the same wall: the app runs, but it is hard to review, hard to secure, expensive to deploy and risky to hand off.

In this article, I’ll decode hidden decisions step by step. I’ll show how to take a fragile AI prototype and make it reviewable, ownable and deployable. You’ll also see how Bit Cloud speeds this up by surfacing scope, infrastructure and delivery artifacts early.

What does production mean in this walkthrough?

Production is when a system becomes transferable and operable without guesswork. The checklist below is what that looks like in practice.

Production means:

  • Clear scope and boundaries: One defined job, explicit dependencies and clear “out of scope.”
  • Known infrastructure and deployment model: Where it runs, how it ships, what it relies on and what costs it creates.
  • Reviewable code structure: Components and responsibilities are readable without starting from the UI.
  • Tests that express behavior: Tests document intent and boundaries so changes stay safe.
  • Documentation that supports handoff: Decisions, assumptions and ops details are written down for a clean transfer.

Starting artifact: Useful output without explicit responsibility

As a concrete starting point, I used a simple frontend checkout prototype built in Replit. The application renders a centered checkout card with a single item, a fixed price and a “Pay Now” action.

The flow completes visually and responds to user input, confirming that the basic interaction works.

Successful payment confirmation screen with amount paid and transaction details.

What’s missing:

This prototype behaves like a demo that processes a request, not a system with explicit responsibility. The plan mentions a database, an API route and server logic, but it does not define the production decisions that make checkout safe and ownable.

Key questions remain unanswered, like who is authorized to pay or view an order, what prevents duplicate charges, what happens on timeouts or partial failures and how the system proves what occurred after the click. Infrastructure and operations are also still implicit, which means deployment, observability and cost drivers are not yet visible.

Reframing the prototype as a system

Instead of treating the prototype as something to be polished, Bit Cloud treats it as something to be structured. The goal is not better output, but making system responsibility explicit as early as possible.

Component tree in Bit cloud

Bit Cloud approaches the prototype as a system-in-waiting. The first step is decomposition. The generated application is broken down into explicit components, execution flows and ownership boundaries.

Design component view in Bit Cloud

What was previously implied by UI behavior is converted into defined responsibilities: where authentication lives, how state is managed, which components own business logic and how external services are integrated.

Component discovery view in Bit Cloud

Hope AI is Bit Cloud’s system intelligence layer, used at this stage not as a prompt engine but as a restructuring tool. Once boundaries and flows are defined inside Bit Cloud, Hope AI regenerates or reorganizes parts of the system to align with that architecture. The output reflects architectural intent rather than raw generated code. Code, tests and documentation are created in the context of the system design managed in Bit Cloud, not in isolation.

At this point, the prototype stops being a single blob of behavior and starts becoming a system that can be reviewed, reasoned about and safely extended. The rest of the walkthrough builds on this foundation.

Within Bit Cloud, Hope AI acts as the engine that turns architectural decisions into structured software. At this stage, it is not used as a prompt engine but as a restructuring tool. Once boundaries and flows are defined, Hope AI regenerates or reorganizes parts of the system to match that structure, producing artifacts that reflect architectural intent rather than raw output. Code, tests and documentation are created as part of the Bit Cloud system design, not in isolation.

Making scope and infrastructure explicit

Once the prototype is reframed as a system, the next step is to surface what was previously hidden: scope, infrastructure and cost. This is where most AI-generated prototypes either gain clarity or accumulate risk. Until these decisions are explicit, progress is based on assumptions rather than engineering judgment.

Defining what services actually exist

The first change is moving from implied behavior to explicit services. Even in a simple checkout flow, this forces clarity. The system is no longer “one app,” but a set of responsibilities with clear ownership.

Components graph in Bit Cloud

This decomposition replaces guesswork with structure.

Making data flow visible

With services defined, data flow can be traced end to end. User input moves through validation, business logic and persistence before producing a response. This makes it clear where state is created, where it is read and where consistency matters. It also exposes failure points that prototypes typically hide, such as partial updates or retry behavior.

Surfacing infrastructure and cost early

Once the system shape is known, infrastructure can no longer remain abstract. The deployment model becomes explicit: what runs as a service, what requires storage and what must scale independently. Compute usage, storage requirements, external API calls and environment separation can all be estimated and discussed early, before they become expensive constraints.

From V1 Alpha to something deployable

At this stage, the system has moved beyond a prototype and into a V1 Alpha. This does not mean the product is finished. It means the system is now structured in a way that allows it to be deployed, reviewed and extended without ambiguity.

Bit Cloud Interface for the payment app.

The V1 Alpha contains concrete engineering artifacts that did not exist in the original prototype. V1 Alpha includes:

Structured code with explicit boundaries

Structured code in Bit Cloud Prototype

The codebase is organized around defined components and responsibilities. UI logic, application logic, data access and integrations are separated so changes can be made deliberately rather than inferred from behavior.

Tests that express expected behavior

Basic tests exist to describe how the system should behave under normal conditions. These tests do not aim for full coverage, but they establish intent and provide a safety net for future changes.

Documentation for ownership and handoff

Key decisions, assumptions and system boundaries are documented. This includes what the system does, what it does not do and where responsibility lies. Another engineer can now review or take over the system without reverse-engineering intent.

A clear deployment path

The system can be deployed outside the prototype environment. The runtime, dependencies and environment configuration are defined well enough to support real deployment, even if further hardening is required.

What is intentionally deferred

Not everything is solved at this stage and that is by design. Performance optimization, advanced security hardening, observability and scale testing are intentionally deferred. These concerns depend on real usage patterns and are expensive to guess prematurely. The V1 Alpha exists to reduce uncertainty, not to optimize prematurely.

What changed from the original prototype
Compared to the initial prototype, the most important change is not visual. It is structural. The original prototype produced behavior without making decisions visible.

The V1 Alpha makes those decisions explicit. Ownership is clear. Flows are traceable. Assumptions are documented. The system can now be reasoned about as a system, not just interacted with as a UI.

Wrapping up

Turning a prototype into a production system is not about replacing speed with process. It is about applying just enough structure at the right moment. When responsibility is explicit, progress becomes predictable and production stops being a leap of faith.

If you’re holding a prototype that “works,” but still can’t be reviewed, owned or deployed with confidence, Bit Cloud helps you make the transition without guesswork. If you want a clearer path from experimentation to deployment, start with Bit Cloud today!

How Data Lake Table Storage Degrades Over Time

The five ways Iceberg table storage degrades over time, from small files to partition skew

This is Part 9 of a 15-part Apache Iceberg Masterclass. Part 8 covered embedded catalogs. This article explains the five ways Iceberg table storage degrades and how to detect each problem before it impacts query performance.

An Iceberg table that works well on day one will not work well on day 365 without maintenance. Every append, update, and delete operation adds files and metadata. Without periodic cleanup and reorganization, query performance gradually deteriorates until someone notices that a dashboard that used to load in 2 seconds now takes 30.

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

Five Types of Degradation

The five ways Iceberg table storage degrades over time, from small files to partition skew

1. The Small File Problem

The small file problem comparing a healthy table with large files to a degraded table with thousands of tiny files

This is the most common and most impactful degradation. Streaming ingestion, micro-batch pipelines, and frequent INSERT operations each create new data files. If these operations produce many small files (under 32 MB), the table accumulates thousands of files where dozens would suffice.

Impact: Each file becomes a manifest entry. A table with 10,000 small files has 10,000 entries that the query planner must evaluate, compared to 40 entries for the same data in properly-sized 256 MB files. Planning time increases linearly with file count.

Cause: Frequent commits with small amounts of data. A streaming pipeline committing every 30 seconds might add 2-3 files per commit, producing 5,000+ files per day.

2. Orphan Files

Orphan files are data files that exist in storage but are not referenced by any current or retained snapshot. They accumulate from:

  • Failed writes: A write that crashes after creating data files but before committing (Part 6) leaves orphan files.
  • Expired snapshots: When snapshots are expired, the metadata references are removed, but the underlying data files remain in storage until explicitly cleaned up.
  • Compaction: When compaction merges files, the old files become orphans after their snapshots are expired.

Impact: Orphan files waste storage space and money. A heavily-written table can accumulate terabytes of orphan files over months. In one common scenario, a daily batch pipeline writing 50 GB per day with weekly compaction can produce 350 GB of orphan files every week. Without cleanup, this costs thousands of dollars annually in storage fees alone.

3. Metadata Bloat

Every commit creates a new snapshot in metadata.json. Over time, the metadata file grows as the snapshot list lengthens. The manifest list for each snapshot may also reference many manifest files, especially if the table has been modified in many different partitions.

Impact: The metadata.json file becomes large, taking longer to download from object storage. At 10,000+ snapshots, the metadata file itself can exceed 100 MB, adding seconds to every query’s planning phase. The manifest list grows, making scan planning slower because there are more manifests to evaluate.

How to detect it: Check the snapshot count using metadata tables. If it exceeds 1,000, configure snapshot expiry to keep the count manageable.

4. Sort Order Decay

If a table has a declared sort order (e.g., sorted by customer_id for efficient lookups), new data written by different engines or pipelines may not respect this sort order. Over time, the min/max statistics per file widen as new unsorted data is mixed with sorted data.

Impact: File skipping becomes less effective. As described in Part 3, tight min/max ranges enable file pruning. Wide ranges mean no files can be skipped. A well-sorted table might skip 95% of files for a filtered query, while the same table with decayed sort order might skip only 10%.

How to fix it: Run compaction with sorting to rewrite files in the correct order and restore tight min/max ranges.

5. Partition Skew

Some partitions grow much larger than others. An event table partitioned by day(event_time) might have 10 GB on a normal day but 500 GB during a promotional event. The oversized partition contains files that are too large or too numerous for efficient processing.

Impact: Queries against skewed partitions are slower because they must process disproportionately more data. Parallel execution becomes unbalanced when one partition’s task takes 50x longer than the others.

Real-World Degradation Timeline

Consider a table receiving 100 small appends per day from a streaming pipeline:

  • Day 1: 100 small files (3 MB each), 300 MB total. Queries are fast.
  • Day 30: 3,000 small files, 9 GB total. Query planning starts to slow noticeably.
  • Day 90: 9,000 small files, 27 GB total. Every query scans all 9,000 manifest entries. Dashboard queries that took 2 seconds now take 15 seconds.
  • Day 180: 18,000 small files plus thousands of orphan files from expired snapshots. Metadata file is 50+ MB. Planning alone takes 10 seconds before any data is read.

Without compaction, the table becomes nearly unusable for interactive analytics within 6 months. With daily compaction, the same table stays at 40-50 well-sized files regardless of how many commits happen each day.

How to Diagnose Table Health

Checklist for diagnosing Iceberg table health using metadata table queries

Iceberg provides metadata tables that let you inspect table health. Here are the key diagnostic queries:

Check File Sizes (Dremio / Spark)

-- Average file size
SELECT AVG(file_size_in_bytes) / 1024 / 1024 AS avg_mb
FROM TABLE(table_files('analytics.orders'))

If average file size is below 32 MB, you have a small file problem. Target: 128-512 MB.

Check Snapshot Count

-- How many snapshots exist?
SELECT COUNT(*) AS snapshot_count
FROM TABLE(table_snapshot('analytics.orders'))

If snapshot count exceeds 1,000, you should expire older snapshots.

Check File Count Growth

-- Files per partition
SELECT partition, COUNT(*) AS file_count
FROM TABLE(table_files('analytics.orders'))
GROUP BY partition
ORDER BY file_count DESC

Partitions with hundreds of files are candidates for compaction.

Dremio supports all Iceberg metadata table queries and provides a SQL interface for monitoring table health.

The Maintenance Imperative

Every Iceberg table in production needs maintenance. The question is not whether to maintain tables but how: manually, through scheduled jobs, or through automated services. Part 10 covers all three approaches in detail.

The cost of not maintaining Iceberg tables is both direct (wasted storage from orphan files) and indirect (slow queries leading to poor user experience, excessive cloud compute costs from reading unnecessary data). Organizations with hundreds of Iceberg tables often find that a single data engineer dedicated to table maintenance saves more in compute and storage costs than their salary. Automated maintenance through Dremio or S3 Tables removes this operational burden entirely.

Books to Go Deeper

Free Resources

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.