Go Unit Testing: Structure & Best Practices

Go’s built-in testing package
provides a powerful, minimalist framework for writing unit tests without external dependencies.
Here are the testing fundamentals, project structure, and advanced patterns to build reliable Go applications.

Why Testing Matters in Go

Go’s philosophy emphasizes simplicity and reliability. The standard library includes the testing package, making unit testing a first-class citizen in the Go ecosystem. Well-tested Go code improves maintainability, catches bugs early, and provides documentation through examples. If you’re new to Go, check out our Go Cheat Sheet for a quick reference of the language fundamentals.

Key benefits of Go testing:

  • Built-in support: No external frameworks required
  • Fast execution: Concurrent test execution by default
  • Simple syntax: Minimal boilerplate code
  • Rich tooling: Coverage reports, benchmarks, and profiling
  • CI/CD friendly: Easy integration with automated pipelines

Project Structure for Go Tests

Go tests live alongside your production code with a clear naming convention:

myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│   ├── helper.go
│   └── helper_test.go
└── models/
    ├── user.go
    └── user_test.go

Key conventions:

  • Test files end with _test.go
  • Tests are in the same package as the code (or use _test suffix for black-box testing)
  • Each source file can have a corresponding test file

Package Testing Approaches

White-box testing (same package):

package calculator

import "testing"
// Can access unexported functions and variables

Black-box testing (external package):

package calculator_test

import (
    "testing"
    "myproject/calculator"
)
// Can only access exported functions (recommended for public APIs)

Basic Test Structure

Every test function follows this pattern:

package calculator

import "testing"

// Test function must start with "Test"
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

Testing.T methods:

  • t.Error() / t.Errorf(): Mark test as failed but continue
  • t.Fatal() / t.Fatalf(): Mark test as failed and stop immediately
  • t.Log() / t.Logf(): Log output (only shown with -v flag)
  • t.Skip() / t.Skipf(): Skip the test
  • t.Parallel(): Run test in parallel with other parallel tests

t.Log is for human-readable test diagnostics. In running services, log/slog and JSON-friendly records are usually a better match for aggregation and incident debugging. See Structured Logging in Go with slog for Observability and Alerting.

Table-Driven Tests: The Go Way

Table-driven tests are the idiomatic Go approach for testing multiple scenarios. With Go generics, you can also create type-safe test helpers that work across different data types:

func TestCalculate(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        op       string
        expected int
        wantErr  bool
    }{
        {"addition", 2, 3, "+", 5, false},
        {"subtraction", 5, 3, "-", 2, false},
        {"multiplication", 4, 3, "*", 12, false},
        {"division", 10, 2, "/", 5, false},
        {"division by zero", 10, 0, "/", 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Calculate(tt.a, tt.b, tt.op)

            if (err != nil) != tt.wantErr {
                t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
                return
            }

            if result != tt.expected {
                t.Errorf("Calculate(%d, %d, %q) = %d; want %d", 
                    tt.a, tt.b, tt.op, result, tt.expected)
            }
        })
    }
}

Advantages:

  • Single test function for multiple scenarios
  • Easy to add new test cases
  • Clear documentation of expected behavior
  • Better test organization and maintainability

Running Tests

Basic Commands

# Run tests in current directory
go test

# Run tests with verbose output
go test -v

# Run tests in all subdirectories
go test ./...

# Run specific test
go test -run TestAdd

# Run tests matching pattern
go test -run TestCalculate/addition

# Run tests in parallel (default is GOMAXPROCS)
go test -parallel 4

# Run tests with timeout
go test -timeout 30s

Test Coverage

# Run tests with coverage
go test -cover

# Generate coverage profile
go test -coverprofile=coverage.out

# View coverage in browser
go tool cover -html=coverage.out

# Show coverage by function
go tool cover -func=coverage.out

# Set coverage mode (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out

Useful Flags

  • -short: Run tests marked with if testing.Short() checks
  • -race: Enable race detector (finds concurrent access issues)
  • -cpu: Specify GOMAXPROCS values
  • -count n: Run each test n times
  • -failfast: Stop on first test failure

Test Helpers and Setup/Teardown

Helper Functions

Mark helper functions with t.Helper() to improve error reporting:

func assertEqual(t *testing.T, got, want int) {
    t.Helper() // This line is reported as the caller
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestMath(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5) // Error line points here
}

Setup and Teardown

func TestMain(m *testing.M) {
    // Setup code here
    setup()

    // Run tests
    code := m.Run()

    // Teardown code here
    teardown()

    os.Exit(code)
}

Test Fixtures

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("setup test case")
    return func(t *testing.T) {
        t.Log("teardown test case")
    }
}

func TestSomething(t *testing.T) {
    teardown := setupTestCase(t)
    defer teardown(t)

    // Test code here
}

Mocking and Dependency Injection

Interface-Based Mocking

When testing code that interacts with databases, using interfaces makes it easy to create mock implementations. If you’re working with PostgreSQL in Go, see our comparison of Go ORMs for choosing the right database library with good testability.

// Production code
type Database interface {
    GetUser(id int) (*User, error)
}

type UserService struct {
    db Database
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.db.GetUser(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}

// Test code
type MockDatabase struct {
    users map[int]*User
}

func (m *MockDatabase) GetUser(id int) (*User, error) {
    if user, ok := m.users[id]; ok {
        return user, nil
    }
    return nil, errors.New("user not found")
}

func TestGetUserName(t *testing.T) {
    mockDB := &MockDatabase{
        users: map[int]*User{
            1: {ID: 1, Name: "Alice"},
        },
    }

    service := &UserService{db: mockDB}
    name, err := service.GetUserName(1)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if name != "Alice" {
        t.Errorf("got %s, want Alice", name)
    }
}

Popular Testing Libraries

Testify

The most popular Go testing library for assertions and mocks:

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

func TestWithTestify(t *testing.T) {
    result := Add(2, 3)
    assert.Equal(t, 5, result, "they should be equal")
    assert.NotNil(t, result)
}

// Mock example
type MockDB struct {
    mock.Mock
}

func (m *MockDB) GetUser(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

Other Tools

  • gomock: Google’s mocking framework with code generation
  • httptest: Standard library for testing HTTP handlers
  • testcontainers-go: Integration testing with Docker containers
  • ginkgo/gomega: BDD-style testing framework

When testing integrations with external services like AI models, you’ll need to mock or stub those dependencies. For example, if you’re using Ollama in Go, consider creating interface wrappers to make your code more testable.

Benchmark Tests

Go includes built-in support for benchmarks:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

// Run benchmarks
// go test -bench=. -benchmem

Output shows iterations per second and memory allocations.

Best Practices

  1. Write table-driven tests: Use the slice of structs pattern for multiple test cases
  2. Use t.Run for subtests: Better organization and can run subtests selectively
  3. Test exported functions first: Focus on public API behavior
  4. Keep tests simple: Each test should verify one thing
  5. Use meaningful test names: Describe what is being tested and expected outcome
  6. Don’t test implementation details: Test behavior, not internals
  7. Use interfaces for dependencies: Makes mocking easier
  8. Aim for high coverage, but quality over quantity: 100% coverage doesn’t mean bug-free
  9. Run tests with -race flag: Catch concurrency issues early
  10. Use TestMain for expensive setup: Avoid repeating setup in each test

Example: Complete Test Suite

package user

import (
    "errors"
    "testing"
)

type User struct {
    ID    int
    Name  string
    Email string
}

func ValidateUser(u *User) error {
    if u.Name == "" {
        return errors.New("name cannot be empty")
    }
    if u.Email == "" {
        return errors.New("email cannot be empty")
    }
    return nil
}

// Test file: user_test.go
func TestValidateUser(t *testing.T) {
    tests := []struct {
        name    string
        user    *User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid user",
            user:    &User{ID: 1, Name: "Alice", Email: "[email protected]"},
            wantErr: false,
        },
        {
            name:    "empty name",
            user:    &User{ID: 1, Name: "", Email: "[email protected]"},
            wantErr: true,
            errMsg:  "name cannot be empty",
        },
        {
            name:    "empty email",
            user:    &User{ID: 1, Name: "Alice", Email: ""},
            wantErr: true,
            errMsg:  "email cannot be empty",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateUser(tt.user)

            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                return
            }

            if err != nil && err.Error() != tt.errMsg {
                t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
            }
        })
    }
}

Useful Links

Conclusion

Go’s testing framework provides everything needed for comprehensive unit testing with minimal setup. By following Go idioms like table-driven tests, using interfaces for mocking, and leveraging built-in tools, you can create maintainable, reliable test suites that grow with your codebase.

These testing practices apply to all types of Go applications, from web services to CLI applications built with Cobra & Viper. Testing command-line tools requires similar patterns with additional focus on testing input/output and flag parsing.

Start with simple tests, gradually add coverage, and remember that testing is an investment in code quality and developer confidence. The Go community’s emphasis on testing makes it easier to maintain projects long-term and collaborate effectively with team members.

See the App Architecture hub for related guides on Go project structure, dependency injection, API design, and integration patterns.

The cognitive bottleneck: rethinking velocity for AI-assisted development

Moving from traditional coding to agentic coding requires a large mindset shift.

As a developer, your job transitions into an orchestrator and reviewer role. You design the ticket, hand it off to an agent like Claude Code to implement, review what was done, and request changes.

A major side effect of this shift is that traditional agile velocity estimations can no longer be calculated the way they once were.

Historically, cost estimates focused mostly on the time it would take a human developer to build out the code. With agentic coding, the time to write becomes an almost non-factor; even a large ticket can take 30 to 60 minutes to implement, instead of days. However, that doesn’t mean you can push out production-ready software in that window.

First, the developer must collaborate with the agent to design what it is going to do. After that comes the heaviest time sink: code review. Reviewing what an AI wrote to ensure it maintains architectural standards and strictly achieves the business intent requires an entirely different type of energy.

The bottleneck is no longer how fast a developer can write code; it is the developer’s cognitive capacity to review it thoroughly.

A notorious adage in software development describes this perfectly (often called Parkinson’s Law of Triviality, or Bikeshhedding): a 5-line code change gets deeply scrutinized in a PR, while a 10,000-line change gets checked off without much thought. This stems directly from the massive cognitive strain that goes into a large code review.

With agentic coding, this cognitive fatigue arises much sooner in the process; occurring continuously during development rather than being back-loaded to a post-development pull request.

Instead of measuring velocity in terms of implementation time, teams must start viewing it through the lens of cognitive complexity.

How do you calculate cognitive complexity?

After a lot of thought about this I think the complexity should be broken up into 5 categories, and within those categories, a rating of low, medium, high.

  • Planning complexity
  • Review surface area
  • Cognitive load
  • Test complexity
  • Ambiguity

1. Planning Complexity

How difficult will the planning session be? How many design decisions need to be locked down before the AI can start?

A simple CRUD endpoint with an obvious implementation path is Low. A feature that touches three architectural layers, requires a new abstraction, or has a data migration is High. Planning complexity determines how long the /plan session takes and how much rework happens if it goes wrong.

2. Review Surface Area

How many files will a reviewer need to read? How many lines of code?

This is measurable. Under five files and 200 lines is Low; a focused change a reviewer can hold in their head. Over ten files or 500+ lines is High; and at that point you’re asking reviewers to maintain the full picture of a system they didn’t design while looking for subtle AI mistakes.

3. Cognitive Load

Review surface area is a reading-time problem. Cognitive load is a comprehension problem. They’re different.

A three-file change that touches your authentication and permission system can be harder to review than a fifteen-file change that adds a new resource with standard CRUD. Cognitive load measures how much system knowledge a reviewer must hold simultaneously to catch the errors an AI is most likely to make.

An isolated, self-contained change is Low. Something that requires understanding a state machine, a set of permission rules, and an async coordination pattern simultaneously is High; because the risk isn’t that any individual piece is hard, it’s that one wrong assumption about how the pieces interact creates a bug that passes all the tests.

4. Test Complexity

AI writes tests too, but someone still has to design the test scenarios, verify that the mocks actually reflect the system’s behaviour, and confirm that edge cases are covered. More scenarios, more external service stubs, and more integration requirements all increase the human overhead here.

5. Ambiguity

This one is different from the others. When a human developer hits an ambiguous requirement mid-implementation, they make a judgment call or ask a question. When an AI hits an ambiguous requirement, it makes a confident guess; and the guess is often plausible enough that nobody catches it in review.

High ambiguity in a ticket doesn’t just add a fixed overhead. It multiplies all the other costs, because a poorly-defined ticket is likely to produce a result that needs reworking; and rework means running the entire planning-review-QA cycle again.

Turning Scores Into a Number

Measuring dimensions is useful, but planning boards need a number. Here’s a formula that works.

Score each execution dimension (Planning, Review Surface, Cognitive Load, Test Complexity) using a non-linear scale; Low=1, Medium=3, High=6; and sum them. This is your base score, ranging from 4 to 24.

The non-linearity is deliberate. Complexity doesn’t scale evenly: a reviewer holding one complex mental model isn’t just 50% more taxed than one holding none; they’re three to four times as taxed, because there’s no margin for error. High=6 reflects that disproportionate cost. Two Highs together compound further still: a reviewer must hold both difficult models simultaneously in a single pass, which is qualitatively harder than doing each separately.

Then apply an ambiguity multiplier:

  • Low ambiguity: ×1.0 (no adjustment; the ticket is well-defined)
  • Medium ambiguity: ×1.25 (some gaps; expect some iteration)
  • High ambiguity: ×1.5 (rework likely; the AI will miss the intent)

Round the result to the nearest integer, then map it to velocity points.

The calibration anchor matters here: these points represent human overhead hours, not implementation hours. Using a conventional 1 point = 4 hours sprint velocity, the mapping looks like this:

Weighted Score Velocity Points Human Overhead
4–9 1 pt ~4 hrs; focused plan + quick review
10–14 2 pts ~8 hrs; a full day of human attention
15–19 3 pts ~12 hrs; plan, review, and QA across 1.5 days
20–23 5 pts ~20 hrs; complex plan + heavy review load
24–28 8 pts ~32 hrs; close to a full person-week
29+ 13 pts Epic; must split before pickup

If your team uses a different anchor (2 hours per point, 8 hours per point), slide the boundary ranges accordingly; the formula stays the same, only the lookup table shifts.

These aren’t implementation hours. They’re human-overhead hours; the time that real people spend planning, reviewing, and verifying work the AI produced.

Calculating on a ticket by ticket basis

This formula isn’t straightforward, and asking developers to calculate this will take time and experience with agentic coding, Once they are familiar with their work habits and experiences with agentic coding, they will be able to figure it out just fine.

Until that happens, Product managers and Dev managers still are going to want to know with some accuracy, what the likely ticket velocity will be, and calculating it based on the amount of coding required won’t arrive at the right answer (or maybe it will but for the wrong reasons)

For that reason I created a small skill for Claude Code that takes a PM built ticket requirement list, and goes through the planning of the ticket, using both the tickets ask and context of the code base, to pre plan the ticket and calculate the weights. This could be something run by a Dev Manager, or Principal developer, or team lead before the ticket reaches a backlog meeting. It could highlight changes that need to be made before presenting the ticket to the team, potential vertically slicing the ticket if its a high complexity, or fold other tickets into one if it’s low.

The plan generated by the estimator wouldn’t be the final plan, that should be left to the developers to work through, and deliver. Developers have their own systems for building features, even when AI is involved. However, an initial plan can give Product Managers the information they need to create sprints that deliver in an agentic space.

Alleviating bottlenecks

Developers can help some of the cognitive overheads by implementing processes during development. Specifically, teams can create agentic review skills that compare generated code against the claude.md specifications, architectural rules, and original ticket plan before a human ever sees the code.

Doing this will mean that the AI agent can self review its code and find holes in it before the developer needs to look at it. Having a First-pass AI skill can help remove the obvious issues and leave the developer only needing to look at the deeper things, rather than having to look for both.

Helpful visual tool

I am providing a link to a helpful estimate tool, so you can see the difference various combinations make to the overall velocity. Hopefully seeing it in action will help it make sense!

https://lawrence72.github.io/claude-tools/calculator/

When a 200-Line CPQ Quote Takes 30 Seconds: Where to Look First

Enterprise deals do not have ten line items. They have two hundred, sometimes more. The same CPQ quote page that loads in 2 seconds for a small quote takes 30 to 45 seconds for a large one. Sales reps avoid editing because every change triggers a recalculation that hangs the browser. Quotes get split into multiple smaller quotes (because the system cannot handle one big one), which then have to be reconciled manually.

The bottleneck is rarely “CPQ is slow in general.” It is usually one of five specific places, and the fix preserves the pricing logic rather than throwing it away.

Bottleneck 1: Price Rules with broad evaluation scope

Price Rules evaluate on quote calculation. Each rule with broad criteria (e.g., “applies to all quote lines”) iterates over every line. With 200 lines and 30 Price Rules, that is 6000 evaluations per recalculation.

The diagnostic: count Price Rules in the org. Count rules with no scope filter or with overly broad scope filters. The combination of high rule count and broad scope is the smoking gun.

The fix: tighten Price Rule scope. Each rule should apply to the smallest set of quote lines it needs. A rule that adjusts discount on a specific product family should filter to that product family, not run for every line. The scope filter is free; not having one is expensive on every recalculation.

Sapota has seen orgs reduce recalculation time by 60 percent through scope tightening alone, without changing any business logic.

Bottleneck 2: Product Rules running unnecessarily

Product Rules validate quote line combinations (constraints like “Product A requires Product B” or “Product C is incompatible with Product D”). They fire on every change, evaluating the full set of lines.

A poorly scoped Product Rule that runs across all lines for every change adds visible latency at the 200-line scale. Same diagnostic as Price Rules: high count, broad scope, high cost per recalculation.

The fix: convert validation-style Product Rules to CML Constraints in RLM (if the migration is in scope), or in CPQ Classic, narrow the evaluation scope per rule. The pattern is identical to Price Rule tightening.

Bottleneck 3: Custom Quote Calculator Plugin (QCP)

CPQ Classic supports a Quote Calculator Plugin written in Apex that runs during calculation. The QCP can add custom logic that the standard pricing engine does not handle natively.

Three patterns make QCP slow at scale:

  • The plugin iterates over quote lines multiple times when one pass would suffice. Refactor to a single pass.
  • The plugin makes SOQL queries inside loops. Bulkify all queries before the loop.
  • The plugin processes lines in serial when parallel processing is safe. Use Apex async patterns where possible.

The QCP is the most common deep performance bottleneck. Code review by someone who knows both CPQ behavior and Apex optimization typically produces a 3 to 5x improvement.

Bottleneck 4: Synchronous calculation on every change

CPQ Classic recalculates on every quote line change. For 200 lines, this means a full recalculation when the rep edits a single discount.

CPQ supports asynchronous calculation: defer the recalculation until the rep explicitly triggers it. The trade-off is that the rep sees stale numbers between changes, but for large quotes this is preferable to a 30-second hang on every keystroke.

The configuration: enable “Calculate quote on calculate action only” in CPQ Configuration Settings. Train reps to click Calculate after a batch of changes rather than after each one. The change is operationally meaningful but acceptable on quotes large enough to need it.

For mid-size quotes (50 to 100 lines), keep synchronous calculation. The threshold for switching depends on the specific org’s rule complexity, but 100 to 150 lines is the common breakpoint.

Bottleneck 5: Smart Approvals queries on large quotes

Approval evaluation runs on quote submission. Smart Approvals (RLM) and Advanced Approvals (CPQ Classic) both evaluate rules against the full quote state. With 200 lines and complex approval criteria, evaluation latency adds to the perceived slowness.

The diagnostic: time the approval evaluation separately from the calculation. If submission takes 30 seconds and calculation took 2, the bottleneck is approval logic, not pricing.

The fix: review approval rule scope. Approval criteria should run on aggregated quote-level state (total amount, discount percentage) rather than iterating per line. Per-line approval criteria are rarely the right design and almost never necessary.

For RLM with Smart Approvals, the new parallel routing feature also helps: independent approval branches can run concurrently rather than serially.

A debugging sequence

The order to investigate a slow quote:

  1. Measure first. Open browser devtools, time the recalculation. Note where the time goes (CPU during calculation, network during save, render after response).
  2. Check Price Rule count and scope. Quick win if the count is high and scope is broad.
  3. Check Product Rule scope. Same diagnostic.
  4. Review QCP code if it exists. This is the deep dive; budget more time.
  5. Try async calculation on a sample quote. If acceptable to the team, the configuration change is small.
  6. Time approval evaluation separately. Address if it is the dominant factor.

Most slow-quote engagements resolve at step 2 or 3 with scope tightening, plus async calculation for the largest quotes. The QCP rewrite is the heavier intervention, reserved for cases where steps 2 to 5 are not enough.

Architecture decisions that prevent the problem

The patterns that keep quotes fast as they scale:

  • Tight scope on every Price Rule and Product Rule. Default to “applies to this product family” rather than “applies to all lines.”
  • QCP only when standard rules cannot do the job. Most QCP code can be replaced with better-scoped Price Rules.
  • Async calculation as the default for new orgs anticipating large quotes. Sync is fine until it is not.
  • Approval criteria at the quote level. Per-line approvals are an anti-pattern.
  • Modular product bundles. A 200-line quote that is actually 20 bundles is faster to manage than 200 individual lines.

Quote performance is one of the most frequent reasons Sapota’s Salesforce team gets called in for an audit. The fixes are repeatable, and the gains are large. A 30-second quote becoming a 5-second quote changes how sales reps use the system.

Debugging quote performance in Salesforce CPQ or RLM? Sapota’s Salesforce team holds the Revenue Cloud Consultant credential and handles performance audits on production engagements. Get in touch ->

See our full platform services for the stack we cover.

Piclu – A button on the kitchen counter that turns drop-in voice notes into a shopping list

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

What I Built

I live with my wife and our 1.5 year old daughter. Both me and my wife are working and usually have busy schedules. My wife manages the stock keeping of grocery and daily essentials and our cook and maid frequently ask her to order stuff that is running out. These requests usually come on-the-fly, and my wife has to keep a mental note and then add it to her ordering list. Later when she gets time, she adds items to cart (usually the likes of Amazon, Blinkit etc). Many times she even forgets items and we end up compromising on some dishes which can’t be prepared because some ingredients are out of stock.

I built Piclu, a product that captures voice notes, and installed it in our kitchen. Now our cook and maid just press a button and leave a voice note whenever something comes to their mind. No runtime interruptions to my wife. The voice notes get transcribed and structured with Gemma 4 E4B, and the shopping list stays populated for my wife to see whenever she gets time. She then orders as appropriate. Our cook and maid are also happier as they don’t have to wait for my wife to be around to share stock outs and they also now have fewer things to keep in their memory.

For the voice capture – I programmed an M5Stack Atom Echo as a Push-To-Talk device and plugged it into the kitchen socket. It records the audio locally and then pushes it to the backend endpoint. Everything runs locally on a small home server, no cloud, so the audio never leaves home.

A fair question here would be: why not just ask the cook and maid to write down the requests or send a WhatsApp voice note? Two reasons. Writing it down is more friction than it sounds when your hands are busy in the kitchen, and not everyone is comfortable writing. And even if they did write it down somewhere, my wife would still have to read through it and add each item to the cart one by one. We tried WhatsApp voice notes as well but it was too much of an ask when they are in the middle of work and in a hurry.

Demo

In the video you can see the full loop – the helper presses the button by the counter and leaves a note in Hindi (“nimbu aur tamatar khatam hai”, out of lemons and tomatoes), Gemma 4 transcribes it onto the list locally, a second Gemma 4 pass turns the notes into a structured shopping list, and each item shows up on its own as a tickable to-do.

Code

GitHub logo

boris93
/
open-piclu

A household voice-note brain powered by local Gemma 4

open-piclu

A household voice-note brain powered by local Gemma 4 E4B. Captures voice notes from anyone in the house (cook, family, helpers), transcribes them in their own language and accent, and turns them into a structured shopping list the household manager can review and act on.

Built for the Gemma 4 Challenge: Build with Gemma 4. Audio arrives over a single HTTP endpoint — bring your own push-to-talk source (the bundled web UI, a Raspberry Pi with a button, a Home Assistant blueprint, an IFTTT webhook, your phone — anything that can POST a WebM/Opus blob to /api/items/freeform_audio).

Data flow

PTT source ─► POST /api/items/freeform_audio   (multipart: audio)
           ─► app HTTP
           ─► brain gRPC over Unix socket
           ─► recordings.Store (seals .webm on disk + emits SealedEvent)
           ─► processor:  ffmpeg decode (WebM/Opus → 16 kHz mono PCM)
                          → Gemma 4 E4B pass 1 — TRANSCRIBE (multimodal audio →
                            Hinglish text,

…

The open sourced repo has the Gemma brain (transcription and structuring), the web UI, and the single audio endpoint that the M5Stack pushes to. It also has the full transcription and extraction eval I talk about below.

Under the hood it is a Go app and brain talking over a Unix socket, the cart and notes update live over SSE, and the whole thing runs from a Docker Compose. Gemma 4 runs locally through a libmtmd / llama.cpp build, with a Lemonade adapter for faster GPU inference (the eval uses the same adapter), and the eval is fully reproducible from the repo.

How I Used Gemma 4

Gemma powers 2 key steps in the note capturing flow – transcription and structuring. Both run as local Gemma 4 E4B passes on the home server.

Step 1: Transcription

When I started building this, I tried a few open source models and put them against each other on a small eval dataset I prepared from our own voice notes – Whisper, indic-whisper from AI4Bharat, and Gemma 4. The pure speech-to-text models had difficulty understanding the accents of our cook and maid when they spoke about our household items. Gemma 4 uniquely stands out, because of its native multimodality – it takes the audio and a text prompt together, so unlike a pure STT model I can actually tell it about what it is about to hear. Across the open-source landscape, Gemma 4 E2B/E4B are among the few models that support native audio and text input within a single prompt.

Among the Gemma variants, the bigger ones (26B MoE and 31B Dense) don’t take audio at all, and the smaller E2B is not accurate enough, its word error rate comes out above 1.0 and it drops about 36% of the items during structuring. So E4B was the variant that is both multimodal on audio and accurate enough for this job. In the eval it also beat Whisper-Large-v3, the gold standard for STT with global language support, both on accuracy and on staying quiet on noise (Whisper tends to invent text on silence, which would quietly pollute the list). Here is the head to head:

Model and prompt WER CER No-speech detection
Gemma E4B, no context in prompt 1.29 0.80 67%
Gemma E4B, full prompt (shipped) 0.55 0.30 93%
Gemma E2B, full context in prompt 1.48 0.90 20%
Whisper-Large-v3 (forced Hindi) 0.70 0.36 7%

Lower WER and CER are better (WER is word error rate, CER is character error rate; CER is the fairer read here because Romanized Hindi has many valid spellings). No-speech detection is how often the model correctly stayed silent or flagged the clip as noise instead of inventing words – the shipped E4B prompt teaches it those no-speech flags, which is why it handles 93%. Full method and the rest of the numbers are in the repo eval.

The context I give it is specific to our home. I live in Gurugram, India and our cook and maid are from West Bengal and Bihar respectively. I prompt E4B with what is being said, the purpose, by whom and intended for whom, so it can steer the predicted tokens keeping the intent in mind. That one change is the single biggest lever – in my eval, adding this context reduced the word error rate by more than half compared to a plain “transcribe this” (the no-context row in the table above). The relevant part of the transcription prompt looks like this:

You are an expert audio transcriber specializing in {{native_language}} dialects.
You receive voice notes from members of a household in {{city}}, {{country}} —
typically the cook, household helps, or family members — addressed to the
household manager regarding the shopping list (stock requests, brand preferences,
quantity changes, cancellations).

The full prompts (transcription and structuring) are in the repo under internal/prompts.

Step 2: Structuring

This pass turns the notes into clean items to be ordered. Notes can arrive with linked context as well, for e.g:

  • Note 1: The dishwash liquid is running out.
  • Note 2: The current dishwash liquid is not good, the earlier one was better.

I keep an SQL table keyed by a name_id, with kind, size, quantity, stock_state and notes. I pass the new transcript and the existing table items to Gemma and it generates a small DSL that the code then parses to insert or update items in the table:

  • Note 1 returns: UPSERT name_id="dishwash-liquid" name="dishwash liquid" stock_state="low"
  • Note 2 returns: UPSERT name_id="dishwash-liquid" name="dishwash liquid" notes="current one is not good, the earlier one was better"

Both notes map to the same name_id, so the table ends up with a single merged row:

dishwash-liquid stock_state="low" notes="current one is not good, the earlier one was better"

If a note is not about the shopping list at all, Gemma emits NO_OP and nothing changes. In the eval, this structuring pass had a recall of 1.0 (it found every product) and an F1 of 0.99, and it never invented items from non-shopping notes.

Next I plan to build MCP servers for the online ordering platforms and then eventually auto populate the carts with the items to be ordered, contextualized using the order history on respective platforms. I am bullish that Gemma 4 can deliver here as well – it will come down to how well it does tool calling and how much its chain-of-thought thinking can optimize the ordering. Let’s see what happens, fingers crossed.

Building High-Converting E-Commerce Stores for Niche Products: A Developer’s Guide

The Challenge of Selling Specialized Products Online

Whether you’re building a store for premium knives, artisanal goods, or other niche products, e-commerce developers face unique challenges. Unlike mass-market retailers, niche stores have smaller audiences but higher margins—making every conversion count. If you’re tasked with building or optimizing a specialty store, here’s what the data shows matters most.

1. Product Photography & Rich Media Strategy

Developers often overlook this, but product images are your most critical UX element for high-value items.

  • High-resolution, multiple angles: Implement lazy loading with WebP/AVIF formats for fast load times
  • Zoom functionality: Use libraries like react-medium-image-zoom for detailed inspection
  • Video embeds: Short clips (15-30s) showing product in use can increase conversion by 40%+
  • Descriptive alt text: Each image needs proper alt text for SEO, not just “product.jpg”
// Lazy-loaded image with modern formats
<img 
  src="knife-thumb.webp" 
  srcset="knife-small.webp 480w, knife-large.webp 1200w"
  loading="lazy"
  alt="Damascus steel hunting knife with 5-inch stainless blade"
/>

2. Structured Data & Rich Snippets

Schema markup is non-negotiable for niche products. Implement:

  • Product schema: name, price, availability, rating
  • AggregateRating: display star ratings if you have reviews
  • BreadcrumbList: essential for category navigation and SEO
  • FAQPage schema: answer common questions (materials, maintenance, warranty)

Well-optimized sites like nazis-duncis.lv/ show how proper schema implementation helps specialty retailers rank for competitive keywords.

3. Core Web Vitals: Performance is Non-Negotiable

For niche products, serious buyers spend time researching. Slow sites kill conversions.

  • LCP (Largest Contentful Paint): aim for < 2.5s
  • INP (Interaction to Next Paint): keep below 200ms
  • CLS (Cumulative Layout Shift): < 0.1 to prevent layout jank

Run Lighthouse audits regularly. Optimize image delivery, defer non-critical JavaScript, and use edge caching for static assets.

4. SEO for Long-Tail Keywords

Niche products dominate through long-tail keywords. Create category pages with:

  • 800-1200 words of guide content (not just product grids)
  • Natural keyword integration: avoid keyword stuffing; write for humans first
  • Contextual internal links: guide readers between related products and buying guides
  • FAQ sections: answer real customer questions with structured data

5. Trust Signals for Premium Items

Buyers spending real money need confidence:

  • Customer reviews with photos: Verified purchase badges increase trust by 73%
  • Clear return policy: Make it prominent and hassle-free
  • SSL/TLS 1.3: Non-negotiable for payment security
  • Credible “About” page: Share your expertise and why customers should buy from you

Key Takeaway

E-commerce for niche products requires balancing technical excellence with psychology. Focus on rich media, blazing-fast performance, structured data, and genuine trust signals. The margins are there—your job is to remove friction.

Test everything. Measure conversions. Iterate.

Monolith vs Modular Monolith vs Microservices: The Honest Decision Framework

Originally published at spectredev.xyz. Cross-posted here for the Dev.to community.

Choosing between monolith, modular monolith, and microservices? Here’s the honest, opinionated framework your startup actually needs. Stop copying Netflix.

Your architecture choice shouldn’t be driven by what Netflix or Uber is doing. It should be driven by where you are right now your team size, your traffic, your deployment maturity, and your runway. The monolith vs microservices debate has a real answer for your situation. It’s just not the one most blog posts give you.

Here’s the framework we use when helping startups and growth-stage companies make this call.

Why Most Teams Get This Wrong From the Start

The mistake I see most often: founders read about how Airbnb or Grab migrated from a monolith to microservices, and they decide to build microservices from day one because that’s what scale looks like.

It isn’t. That’s what post-scale looks like. There’s a difference.

When those companies broke apart their monoliths, they had hundreds of engineers, mature CI/CD pipelines, dedicated platform teams, and years of operational experience with their own domain boundaries. They weren’t starting fresh. They were solving a problem that emerged from growth, not anticipating one that might never arrive.

Starting with microservices before you have product-market fit is one of the fastest ways to burn engineering resources on infrastructure instead of product. We’ve seen it happen. It’s painful to watch.

The Monolith: Unfairly Maligned

A monolith isn’t a bad architecture. It’s a starting point. And for most teams honestly, for teams up to 10–15 engineers with a single product it’s the right one.

In a traditional monolith, all your application code lives in one deployable unit. One codebase, one database, one deployment pipeline. The benefits are real and often underappreciated.

Development velocity is genuinely faster early on. There’s no network latency between services, no distributed transaction complexity, no service discovery overhead. You can refactor across the entire codebase in one shot. Debugging is straightforward because everything runs in one process.

The problems come later. As your team grows, the codebase becomes harder to navigate. Different teams start stepping on each other’s work. Deployments get slow and risky because every change ships everything. That’s when the architecture starts to fight you, not help you.

The real signal you’ve outgrown a monolith isn’t traffic. It’s team friction and deployment pain.

How to build a backend that scales from 100 to 10 million usershow-to-scale-startup-backend)

The Modular Monolith: The Option Nobody Talks About Enough

Here’s the counter-intuitive point: for most startups that think they need microservices, a modular monolith is actually the better answer.

A modular monolith is still a single deployable unit, but the internals are deliberately structured into isolated modules each with clear boundaries, their own data ownership, and strict rules about how modules interact. Think of it as microservices discipline inside a monolith’s deployment model.

The practical result: you get much of the architectural clarity of microservices without the operational overhead. You can enforce team ownership of modules. You can move faster without breaking unrelated parts of the system. And when you eventually decide to extract a service, the module boundary makes it a surgical operation instead of a painful untangling.

Shopify ran a monolith for years, and the work they did to make it modular what they called “componentisation” is one of the more honest engineering stories out there. It wasn’t glamorous. It was just effective.

A modular monolith is the architecture that earns microservices. You build the discipline first, then you extract services when the operational case is clear.

Microservices: When They Actually Make Sense

Microservices are the right answer for a specific set of conditions. All of them need to be true, not just one.

Your team is large enough that different groups genuinely need independent deployment cycles. You have parts of your system with radically different scaling characteristics say, a real-time notification service that spikes to millions of events per second while your invoicing service handles hundreds of requests per day. You have the platform engineering capacity to run container orchestration, service meshes, distributed tracing, and on-call rotations for multiple services. Your domain boundaries are well understood because you’ve built and operated the system long enough to know where they should be.

If you’re missing any of those, microservices will slow you down.

The operational surface area is real. In a distributed system, you’re now debugging network partitions, handling partial failures, managing schema migrations across service boundaries, and coordinating deployments across multiple repositories. Each of those is a solvable problem. Collectively, they require a team that has the headspace to solve them.

One of SpectreDev’s clients a Series A fintech came to us after attempting a microservices migration with a team of six engineers. Eighteen months in, they had eight services, three of which couldn’t be deployed independently because of undocumented shared state. The team was spending more time on infrastructure incidents than feature work. We spent three months collapsing it back to a modular monolith before rebuilding the extraction incrementally. The irony isn’t lost on anyone.

What is database sharding and when does your startup actually need it

The Decision Framework

Use this as a starting point. It’s opinionated and it’s supposed to be.

Build a monolith if: you’re pre-product-market fit, your team is under 8 engineers, and your primary constraint is development speed.

Refactor toward a modular monolith if: you’ve found product-market fit, your team is growing, and you’re starting to feel the organisational friction of a shared codebase but you don’t yet have the platform maturity for distributed systems.

Extract services from that modular monolith if: a specific module has a genuinely different deployment cadence, a clearly different scaling profile, or a different team ownership model that justifies the operational overhead. Extract one service, operate it well, then decide on the next.

Notice what’s not on that list: “because we expect to have 10 million users someday.” That’s not an architecture decision. That’s wishful thinking. Architect for where you are and the next logical growth phase, not for a ceiling you may never approach.

For Indonesian companies specifically, there’s an additional layer to consider: talent availability. Engineers comfortable with distributed systems operations Kubernetes, service mesh, distributed tracing are a thinner slice of the market in Jakarta than in San Francisco. A modular monolith your current team can operate confidently is worth more than a microservices setup that creates a hiring dependency you can’t fill.

Practical Example: How a Regional E-Commerce Startup Should Think About This

Consider a regional e-commerce platform think something operating across Indonesia, Malaysia, and the Philippines in the 50,000–200,000 active users range and growing.

At that scale, the right architecture is almost certainly a modular monolith. You’d want clearly isolated modules for the product catalogue, order management, payments (especially given regional payment methods like GoPay, OVO, and GrabPay that each have their own integration logic), and logistics tracking.

None of those need independent deployments yet. But structuring them as modules means when you hit 2 million users and the payments module is getting hammered during 11.11 flash sales, you can extract it as a standalone service with a clear API contract already in place. The groundwork is done.

The alternative building microservices for each of those domains at 50,000 users would add months of infrastructure work before you’ve even proven the product works in all three markets.

How to build a backend that scales from 100 to 10 million users

FAQ

Q: Can I start with a monolith and migrate to microservices later without rewriting everything?

A: Yes and this is actually the intended migration path. The key is maintaining clean module boundaries inside your monolith from early on. If you’ve built a well-structured modular monolith, extracting a service means defining the API boundary (it probably already exists as a module interface), setting up the deployment infrastructure for that service, and gradually moving traffic. It’s still significant work, but it’s not a rewrite. A “big bang” monolith with tangled dependencies is the one that requires a painful rewrite.

Q: At what team size should I seriously consider moving to microservices?

A: Team size isn’t the only variable, but a rough heuristic: when you have more than 15–20 engineers working on the same codebase and deployment friction is measurably slowing you down, it’s worth having the conversation. The more useful indicator is whether you can deploy changes to one part of the system without risking unrelated parts and whether the answer is “no” consistently enough to hurt you.

Q: Are microservices harder to secure than a monolith?

A: Differently hard, not necessarily harder. In a monolith, your attack surface is more contained but a compromise can affect the whole system. In microservices, you have more network attack surface and need to secure service-to-service communication (mTLS, service accounts, network policies). The security posture depends entirely on your implementation. Neither architecture is inherently more secure.

Q: What about serverless is that a fourth option?

A: Serverless functions can be a useful pattern within any of these architectures, but they’re not a replacement architecture. You can have serverless functions inside a modular monolith (for async event processing, for example), and you can have them inside a microservices system. Serverless introduces its own complexity around cold starts, stateless design, and vendor lock-in that most teams underestimate. For most startups, it’s a tool, not a strategy.

Q: We’re a non-technical founder. How do we evaluate whether our current tech team is recommending the right architecture?

A: Ask them to explain the trade-offs, not just the choice. A strong engineer can tell you what you’re giving up with their recommended approach, not just what you’re gaining. If the pitch for microservices doesn’t include an honest discussion of operational overhead, distributed system complexity, and your team’s current capability to manage it that’s a flag. The best architecture recommendation for your stage should feel slightly boring. Exciting architecture choices are usually expensive ones.

The right architecture is the one that lets your team ship product, maintain reliability, and adapt as your business changes. That’s not microservices for most of you reading this. It’s probably a well-structured monolith or modular monolith that gives you the discipline to grow into something more distributed when the evidence actually demands it.

If you’re at the stage where these decisions are becoming real whether you’re building 0 to 1 or hitting the walls of a system you’ve outgrown this is exactly what we work through with clients at SpectreDev.

What Developers Don’t Say in Interviews—but Show on GitHub

GitHub Mining

When I started working on my usability study project with KServe, I interacted with KServe users to understand the challenges they were experiencing while using the platform. During these conversations, users frequently mentioned GitHub issues and the problems they encountered during development and deployment.

This led me to explore how engineers solve problems by using GitHub repositories and collaborating in open-source communities. As part of my research, I started reading articles and academic research papers online, and I came to know this is the best approach and many organizations are already following this methods to identify the developer problems.

GitHub mining (also called repository mining, issue mining, or software repository mining) is increasingly used as a user research and UX research method in open-source ecosystems because it allows researchers to understand user challenges from real-world interactions rather than only interviews or surveys.

Here is the research paper links: 1. Mining Developer Behavior Across GitHub and Stack Overflow (Xiong et al., 2017)., 2. Understanding Java Usability by Mining GitHub Repositories (Lemay, 2018), 3. Insights from GitHub Community on the Matter Standard: Developer Perspectives and Challenges (Hassan, 2026).

I began exploring the GitHub mining process alongside conducting 1:1 usability study sessions with users. Through this research, I learned a great deal about how developers work, collaborate, report issues, and solve problems in open-source communities.

In this article, I am going to explain:

  • What the GitHub mining process in user research is?
  • Why it is important to conduct this type of research?
  • How it helps identify developer pain points?
  • How the findings can support communities and developers in improving the overall developer experience (DX) and usability of open-source tools like K-Serve.

Let’s start…

What is GitHub Mining in User Research?

GitHub mining is a research method that systematically collects and analyzes data from GitHub repositories—such as issues, pull requests (PRs), discussions, comments, commits, documentation changes, and feature requests—to understand how people use software and where they experience problems.

GitHub Mining

From a UX perspective, GitHub becomes a large archive of user feedback and user behavior.

Instead of asking users directly:“What usability problems do you face?”

researchers examine: Bug reports, Questions, Configuration failures, Feature requests, Support discussions, Workarounds, Documentation complaints, Delayed PR discussions, Community conversations, These become signals of user experience.

Why is GitHub Mining Conducted?

GitHub mining is conducted because traditional UX methods alone do not always reveal the full picture. Open-source projects often have: thousands of users, globally distributed contributors, limited access to end users, asynchronous communication. GitHub provides a historical record of actual user struggles.

GitHub Mining

Researchers conduct GitHub mining to: Discover usability issues at scale, Understand real user behavior, Study product evolution over time, Generate evidence-based design decisions

1. Discover usability issues at scale:

Researchers conduct GitHub mining because it allows them to study user experiences and product challenges at a much larger scale than traditional research methods. Instead of interviewing a small group of users—such as 10 participants in interviews or usability sessions—researchers can analyze hundreds or even thousands of GitHub issues, pull requests, discussions, and feature requests to identify recurring patterns across an entire user community.

For example, researchers may discover that among all reported issues, 300 are related to deployment problems, 100 focus on observability complaints, and 50 request better documentation. Looking at this data collectively helps reveal trends that individual interviews may not uncover. This large-scale approach makes it easier to identify which problems occur most frequently and which areas of the product create the greatest friction for users.

2. Understand real user behavior:

Another important reason researchers use GitHub mining is to understand real user behavior rather than relying only on assumptions or controlled testing environments. When users open issues on GitHub, they often describe what they were trying to achieve, where they became stuck, what configuration mistakes they made, which expectations were unmet, and which onboarding barriers prevented them from completing tasks successfully.

These issue discussions provide a detailed record of actual workflows and real-world usage conditions. For example, a developer deploying a tool may explain that installation instructions were unclear, required settings were missing, or error messages did not provide enough guidance. By studying these interactions, researchers gain insight into how users actually experience a product rather than how designers assume they use it.

3. Study product evolution over time:

GitHub mining also helps researchers study product evolution over time. Because GitHub maintains historical records of issues, discussions, fixes, and releases, researchers can observe when specific problems first appeared, how long they remained unresolved, and whether implemented solutions reduced future complaints.

This longitudinal view helps teams evaluate whether design improvements, documentation updates, or technical changes created measurable improvements in usability and developer experience. For example, researchers can compare issue frequency before and after a deployment redesign to determine whether users encountered fewer deployment failures after the change.

4. Generate evidence-based design decisions:

Finally, GitHub mining supports evidence-based design decisions. Instead of making design changes based on assumptions or subjective opinions. for example, saying, “Users seem confused during deployment”—researchers can present measurable findings supported by real community data.

They may conclude that “37% of reported issues involve deployment discoverability,” which provides a stronger foundation for prioritizing product improvements. This evidence helps UX researchers, product teams, engineers, and open-source maintainers make informed decisions about where to invest effort, improve usability, reduce developer friction, and create better experiences for the broader community.

When Should GitHub Mining Be Conducted?

GitHub mining is valuable during multiple stages.

GitHub mining can be conducted at different stages of the research and product development lifecycle, with each stage serving a specific purpose in improving usability and user experience. During the discovery phase, researchers use GitHub data to identify user pain points by analyzing issues, discussions, and feature requests. This helps teams understand the most common challenges users face before making design or development decisions.

GitHub Mining Be Conducted

Before conducting surveys or interviews, GitHub mining can be used to generate hypotheses. Instead of starting research with assumptions, researchers review historical issue data to identify patterns and form evidence-based questions. For example, if many users report deployment difficulties, researchers may design interview questions specifically around deployment workflows and onboarding experiences.

During a product redesign, GitHub mining helps teams validate recurring issues and confirm whether previously identified problems continue to affect users. This ensures redesign efforts focus on meaningful improvements rather than isolated opinions. By reviewing historical and current issue reports, teams can prioritize areas that consistently create friction.

GitHub mining is also valuable for continuous UX monitoring, where researchers regularly track issue trends to measure overall usability health. Ongoing analysis allows teams to detect emerging problems early, observe changes in user sentiment, and monitor whether user experience improves or declines over time.

After a product update or feature launch, GitHub mining supports post-release evaluation by helping teams measure the impact of changes. Researchers can compare issue volume and themes before and after release to determine whether updates reduced complaints, improved workflows, or introduced new challenges.

Finally, GitHub mining is especially useful in longitudinal studies, where researchers analyze data across extended periods to understand trends over time. This allows teams to observe how user needs evolve, how products mature, and whether long-term improvements lead to sustained reductions in usability issues.

Through these stages, GitHub mining becomes a continuous source of evidence that supports data-driven decisions and helps create better experiences for users and developer communities.

How Does the Community and Developers Benefit?

GitHub mining provides several important benefits for developers because it helps teams make decisions based on actual user experiences rather than assumptions.

One of the major advantages is better prioritization. By analyzing issue reports, discussions, and recurring complaints, developers can identify which problems affect the largest number of users and focus their efforts on fixing the areas that create the most difficulty. Instead of allocating time based only on internal opinions, teams can prioritize improvements that deliver the greatest value to the community.

Community and Developers Benefit

Another important benefit is reduced support burden. When developers repeatedly analyze and address the root causes of commonly reported issues, users encounter fewer recurring problems and require less direct support. Over time, this reduces duplicate issue reports, lowers maintenance effort, and allows development teams to spend more time building new features rather than responding to the same questions repeatedly.

GitHub mining also contributes to improved onboarding for new users and contributors. By identifying patterns in issue reports related to installation challenges, setup confusion, missing documentation, or early-stage errors, teams can improve guidance materials and simplify user workflows. These improvements create a lower learning curve, helping users become productive more quickly and reducing frustration during initial adoption.

Finally, GitHub mining supports data-driven decisions across product planning and development. Rather than creating roadmaps based on assumptions about what users might need, teams can use measurable evidence from issue trends and community feedback to guide future work. This makes product roadmaps more strategic, more transparent, and more aligned with actual user needs, ultimately leading to stronger developer experience and more effective product evolution.

Benefits for Open Source Communities

GitHub mining and usability research provide several important benefits for open-source communities by helping projects become more accessible, sustainable, and user-centered.

Open Source Communities

One key benefit is creating a healthier contributor experience, where new contributors can understand project workflows, contribution processes, and technical expectations more quickly, reducing barriers to participation. These improvements also support higher adoption, because better usability makes projects easier to learn and use, attracting more users and contributors over time.

In addition, insights gathered from user issues and discussions lead to better documentation, allowing communities to create more targeted and practical guidance that addresses real user challenges instead of assumed needs. Together, these improvements contribute to increased retention, as users and contributors are more likely to remain active in a project when frustration decreases and their experience becomes smoother and more productive.

Benefits for Product Teams

GitHub mining and usability research provide valuable advantages for product teams by creating a stronger connection between user feedback and product decisions. One major benefit is providing evidence for redesign, allowing teams to make improvements based on actual user-reported issues instead of assumptions or internal opinions.

Product Teams

This research also helps establish usability benchmarks, enabling teams to measure and compare user experience over time and determine whether product changes are producing meaningful improvements.

In addition, GitHub data creates a continuous feedback loop, where ongoing issue reports, discussions, and community input help teams identify emerging problems and continuously refine the product experience. Finally, it supports release quality monitoring by allowing teams to evaluate how updates perform after launch, detect new usability concerns early, and measure whether releases successfully reduce user friction and improve overall product quality.

Are UX Researchers and Organizations Adopting This Method?

UX researchers and organizations are increasingly adopting repository mining and GitHub mining as research methods, especially as software development becomes more collaborative, distributed, and community-driven. Traditional research approaches such as interviews and usability testing remain valuable, but researchers now complement them with large-scale behavioral data collected from repositories to better understand how people actually use and contribute to software products.

Organizations Adopting This Method

This approach is becoming common across areas such as Open-Source UX Research, Developer Experience (DevEx), Human–Computer Interaction (HCI), Software Engineering Research, Empirical Software Studies, and AI/ML Operations Usability, where understanding real-world workflows and developer challenges is essential.

Rather than relying on a single research method, many research communities and organizations use a mixed-method approach that combines repository mining, interviews, surveys, ethnographic observation, and telemetry data. Combining multiple methods improves research validity because findings can be verified from different perspectives—what users say, what users do, and what product data shows.

Through this approach, researchers aim to understand developer pain points, improve onboarding experiences, measure usability debt (the accumulated usability problems that slow users down), and support evidence-based product decisions. As a result, organizations can design products that better reflect actual user needs, strengthen contributor experiences, and continuously improve overall usability and developer experience.

Why This Method Is Growing in UX Research?

GitHub mining is becoming increasingly important in UX research because modern digital products extend beyond traditional consumer applications and now include complex technical environments such as cloud platforms, Kubernetes ecosystems, AI platforms, and DevOps tools.

Traditional UX research has often focused on studying end users through interviews, surveys, and usability testing. However, modern software systems require researchers to also study developers as users, since developers interact directly with interfaces, documentation, workflows, configuration systems, and deployment processes as part of their daily work.

Growing in UX Research

GitHub mining supports this shift by allowing researchers to observe user experience through engineering artifacts—such as issue reports, discussions, pull requests, feature requests, and contribution patterns—rather than relying only on questionnaires or self-reported feedback. These artifacts provide evidence of where users become confused, what tasks create friction, which expectations are unmet, and how workflows perform in real environments.

A strong research framing would be: “GitHub issues are not only defect reports; they represent observable traces of user experience and can be analyzed as usability evidence.”

This perspective aligns closely with modern practices in UX research, Human–Computer Interaction (HCI), and Developer Experience (DevEx) studies, where understanding real user behavior and evidence-based decision-making has become increasingly important.

Conclusion:

Through my K-Serve usability research, I learned that understanding developer experience requires looking beyond traditional interviews and usability testing. By combining 1:1 user sessions with GitHub mining, I was able to observe real-world developer challenges through issues, discussions, pull requests, and community collaboration.

This approach showed that GitHub repositories are not only places where technical problems are reported—they also contain valuable evidence of user experience, usability barriers, and product friction. GitHub mining helps researchers, product teams, and open-source communities make more informed, evidence-based decisions that improve usability, reduce developer pain points, strengthen onboarding, and create better developer experiences (DX).

As modern software ecosystems continue to grow in complexity, repository mining is becoming an increasingly valuable method for understanding how people truly work, collaborate, and build in open-source environments.

DeepSeek V4-Pro Just Got 4x Cheaper. But Here’s What Nobody’s Talking About

DeepSeek V4-Pro Just Got 4x Cheaper. But Here’s What Nobody’s Talking About

DeepSeek dropped a bombshell on May 22: the 75% discount on V4-Pro is now permanent.

Was Now
Input (cache miss) $1.74 / 1M tokens $0.435 / 1M tokens
Output $3.48 / 1M tokens $0.87 / 1M tokens

That’s 20–35x cheaper than GPT-5.5. If you’re building AI agents or running automated coding pipelines, this changes everything.

The HN thread hit 433 points and 248 comments. Developers are excited. But there’s a catch almost nobody is discussing.

The Silent Problem: Single-Key Rate Limits

Here’s what happens when you actually try to use DeepSeek at scale with the new pricing:

[ERROR] 429 Too Many Requests

Every DeepSeek API key has a rate limit. When you’re running Claude Code, Cline, or any AI agent loop that fires off dozens of requests per second, you’ll hit that wall fast.

And when you hit it, your workflow stops. Dead.

The Fix: Multi-Key Load Balancing with Automatic Failover

The solution is conceptually simple but tricky to implement well:

┌─────────────┐     ┌──────────────────┐
│  Your App    │────▶│  Load Balancer   │
│  (Claude     │     │  (One-API /      │
│   Code, etc) │     │   custom proxy)  │
└─────────────┘     └──────┬───────────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐ ┌─────────┐ ┌─────────┐
        │ Key #1  │ │ Key #2  │ │ Key #3  │
        │ $5      │ │ $5      │ │ $5      │
        └─────────┘ └─────────┘ └─────────┘

Here’s how it works:

  1. Round-robin distribution — spread requests across multiple keys so no single key hits the limit
  2. Automatic failover — if Key #1 returns 429, the request automatically retries on Key #2
  3. Transparent to your app — just point your OPENAI_BASE_URL at the proxy, keep using the same API format

Option 1: Roll Your Own

You can set this up with One-API (open source, Docker-friendly):

docker run -d -p 3000:3000   -e CHANNEL_TYPE=deepseek   -e CHANNEL_KEYS=sk-key1,sk-key2,sk-key3   justsong/one-api

Then configure multiple DeepSeek API accounts, each with its own key. One-API handles the load balancing and failover transparently.

Caveat: You need to manage key rotation yourself, monitor balance across accounts, and handle the ops overhead.

Option 2: Use a Managed Proxy

If you don’t want to run Docker containers and monitor key balances, there are services that handle this for you.

One option is AiCredits, which pools multiple DeepSeek keys behind a single endpoint with built-in failover. Same OpenAI-compatible API. Same DeepSeek models. But with redundancy baked in.

The tradeoff is a small markup over direct pricing — but you’re paying for:

  • Automatic failover when keys hit rate limits
  • No need to manage multiple accounts
  • No Docker containers to maintain

What This Means for AI Agents

The real killer use case for DeepSeek V4-Pro at $0.87/M output is autonomous AI agents.

Claude Code, Cline, OpenCode — these tools fire off hundreds of API calls per session. With GPT-5.5 at $30/M output, a heavy coding session could cost $20+. With DeepSeek V4-Pro, the same session costs under $1.

But only if your setup can handle the throughput. Single-key setups will choke. Multi-key with failover won’t.

The Bottom Line

DeepSeek V4-Pro’s permanent 75% price cut is the biggest AI pricing event of 2026. But extracting maximum value requires solving the rate-limit bottleneck.

Whether you DIY with One-API or use a managed proxy, the important thing is: don’t build your agent pipeline on a single key.

What’s your setup for handling DeepSeek rate limits? Let me know in the comments.

How I Built a Full-Stack Roulette Game with Claude AI and Deployed It to AWS — While Learning Everything Along the Way

I wanted to build a roulette website. I also set out to learn.

I have worked in IT for some time, started with Windows admin, moved to Linux, and then onto some basic DevOps work. I may have gotten to the 4th chapter of every Python book ever written, but never fully committed to programming. I have never really done every aspect of the build process, the “Full Stack.”

I wanted to understand the full picture — how a modern web application actually gets built, containerized, deployed, and maintained in production. Not just the code, but the infrastructure. Docker. Reverse proxies. SSL certificates. DNS. The things that tutorials skip over and production demands.

I also wanted to test a theory: could I use AI — specifically Claude — as a genuine development partner to build something real? Not to generate a throwaway script, but to architect, debug, and ship a production application across multiple servers, frameworks, and deployment environments?

So I built Big Spin Fun, a free online roulette game with European, American, and Triple Zero variants. It runs on Next.js, Django, PostgreSQL, and Nginx, all containerized with Docker Compose and deployed to AWS Lightsail. And I built virtually all of it through a conversation with Claude.

Here’s how it actually happened — the messy, nonlinear, real version.

Why Roulette?

I’ve always been a casual gambler with a genuine interest in the mechanics of different betting systems. I’m a regular viewer of channels like CEG Dealer School, The Roulette Master, and Jack Ace, and I found myself wanting a sandbox where I could test out strategies without any financial risk. In the end, building my own simulator felt like a much more rewarding challenge than simply clicking around on someone else’s site.
Claude as a Development Partner
Let me be upfront about this: Claude wrote the vast majority of the code in this project. But that sentence undersells what actually happened.

I didn’t paste in a prompt that said “build me a roulette game” and get back a finished product. The project evolved over a long, iterative conversation that spanned the full lifecycle of a real application — from a single React component to a multi-container Docker deployment with HTTPS, user authentication, and SEO optimization.

What made it work was that I brought the vision and the decisions, and Claude brought the implementation knowledge. I’d say “I want user accounts and a database,” and Claude would lay out the options — Django vs Flask, Postgres vs SQLite — explain the tradeoffs, and then build it once I chose a direction. When something broke, I’d paste the error, and Claude would diagnose it.

This wasn’t autopilot. I had to understand what was happening at every step. When Claude generated a Dockerfile, I needed to understand multi-stage builds to know why it was structured that way. When it wrote a Django model, I needed to understand what migrations were to debug why the database wasn’t creating tables. The AI accelerated my learning — it didn’t replace it.

The conversation itself became a kind of documentation. Every architectural decision, every debugging session, every “why did I get a 502 Bad Gateway” was captured in the chat history. I could scroll back and see exactly why we chose Next.js over plain React (SEO), why we added Nginx in front of everything (SSL termination and security), and why the database password couldn’t contain a + character (URL parsing).
Starting on Windows with VS Code
The project started on my Windows machine (using Windows Subsystem for Linux) with VS Code open and a vague idea: build a roulette wheel that spins. That was it. No grand architecture plan, no deployment strategy, just curiosity.

I opened Claude and described what I wanted. Within minutes, I had a working React component — a spinning SVG wheel with authentic pocket ordering, a grid of clickable numbers, chip selection, and payout calculations. It ran in Vite on localhost. You could click a number, hit spin, and watch an animated wheel land on a random pocket.

That first component was maybe 400 lines of JSX. Claude generated the entire thing — the SVG math for the wheel segments, the CSS animations for the spin physics, the betting logic, the color mapping for red and black numbers. Things that would have taken me days of MDN documentation and Stack Overflow to figure out came together in a single response.

But here’s the thing: I immediately started asking questions. “What language is React?” “How do I make changes to this?” These weren’t embarrassing questions — they were the natural learning path of someone using AI to build beyond their current skill level. And Claude answered them without judgment, at exactly the level of detail I needed.

VS Code was my constant companion through every phase. The search and replace functionality alone probably saved me hours. When you’re renaming things across a full-stack app — changing “Roulette Royale” to “Big Spin Fun” across frontend pages, meta tags, Docker configs, and Nginx files — Ctrl+Shift+H becomes your best friend.
The Docker Obsession
Docker is the cornerstone of the modern-day app in the cloud. I could have stopped at a single React app served by Nginx. But I wanted to understand Docker. Not just “run a container,” but actually architect a multi-container application the way production systems work.

I told Claude I wanted everything in Docker containers, very portable, something I could eventually move to AWS. Claude scaffolded a multi-stage Dockerfile — Node builds the React app, then only the compiled static files get copied into an Nginx Alpine image. The result was about 25MB. That felt satisfying.

But then I started thinking bigger. I wanted user accounts. A database. Server-side game logic so players couldn’t just edit their balance in the browser console. I asked Claude about Django, and that led to a conversation about architecture that fundamentally changed the project.

I am a perpetual beginner when it comes to Python. I have always had the intent to get more comfortable with it, so moving to Django seemed like a logical step

Claude didn’t just say “use Django.” It explained the tradeoffs. Django would give me auth, an admin panel, and an ORM, but the frontend would still need React for the interactive game. I’d end up with two containers plus a database. Was I ready for that complexity? I said yes.

That “yes” turned a weekend project into a real full-stack application:

Next.js for the frontend — Claude recommended it over plain React because I mentioned wanting SEO and advertising revenue eventually. That one recommendation shaped the entire frontend architecture.
Django REST Framework for the backend API — user auth, game engine, leaderboard
PostgreSQL for persistent data
Nginx as a reverse proxy in front of everything

Four services. One docker-compose.yml. Claude generated all of it — the Dockerfiles, the compose configuration, the Django models, the API views, the Next.js pages, the Nginx config. But each piece came through conversation, not a single prompt.
Docker Compose: Simple in Theory, Brutal in Practice
The docker-compose.yml looks clean when it’s done. Four services, some environment variables, a few volume mounts, a health check. Maybe 50 lines of YAML.

Getting to those 50 lines was anything but clean. And this is where working with Claude on real infrastructure problems — not textbook examples — taught me the most.
The Rocky Linux Detour
After getting everything working locally on Windows, I deployed to a Rocky Linux server. This was my first time running Docker Compose on a remote machine, and the workflow was clunky.

I’d edit files locally in VS Code, push to GitLab, SSH into the Rocky server, pull the changes, rebuild, and check if it worked. When it didn’t — and it often didn’t — I’d SSH back in, read the logs, paste them into Claude, get a fix, go back to VS Code, make the change, push, pull, rebuild.

Claude was patient through all of this. The same kinds of errors would come up in slightly different forms, and it would adjust its advice based on what we’d already tried. It remembered context from earlier in the conversation — “we fixed the migration issue already, so this is a different problem” — which kept the debugging focused.

Rocky Linux worked fine as a Docker host, but when I decided to move to AWS, Ubuntu was the more natural choice for Lightsail.
Moving to AWS Lightsail
Lightsail is AWS’s simplified VPS offering. You pick a size, pick an OS, and you get a server with a static IP. No VPC configuration, no security group mazes, no IAM policy debugging. Claude recommended it as the simplest path to AWS — it’s AWS for people who just want a server.

Claude walked me through the entire setup: SSH key generation, installing Docker on Ubuntu, cloning from GitLab using SSH keys, configuring DNS. Each step was a short exchange — I’d ask “how do I do X,” Claude would give me the exact commands, I’d run them, report back, and move on.

When the build hung on the small Lightsail instance, Claude suggested the GitLab Container Registry — build on my local machine, push images to GitLab’s free registry, pull pre-built images on the server. The server only needs enough RAM to run the containers, not build them.

Before: build on the server (needs RAM)

frontend:

build: ./frontend

After: pull pre-built images (needs almost no RAM)

frontend:

image: registry.gitlab.com/booker357/roulette-page/frontend:latest

Eventually I added swap space and switched back to building on the server directly, which simplified the workflow. Edit files on Lightsail, build on Lightsail, push to GitLab for version control. One machine, one workflow.
The Migration Problem
Django uses migration files to create database tables. These files need to exist in the codebase before the application starts. Claude had generated the models and the views, but the migration files — the glue between the code and the database — were missing from the project.

The cycle went like this: start the app, Django crashes because the accounts_player table doesn’t exist, paste the error into Claude, get told to run makemigrations, run it inside the container, migrations get created in the container’s filesystem, stop the container, migrations disappear, start the container, crash again.

This went on for multiple rounds. Each time I’d paste the error into Claude, and each time we’d get a little closer to understanding the root cause. Claude eventually realized the issue: the migration files were being generated inside ephemeral containers and never persisted to the host filesystem. The fix was to create the migration files directly and commit them to the repo.
I was, admittedly, pretty dependent on Claude.

Claude then generated the complete migration files by hand — reading the Django models it had written earlier and producing the corresponding migration Python code. It was like watching someone debug their own work in real time.

This was a turning point in how I thought about AI-assisted development. Claude had made an oversight in the initial scaffolding (not including migration files), but it was also the one that diagnosed the problem, explained why it was happening, and generated the fix. The AI wasn’t perfect, but it was resilient — each error message I fed back in got us closer to a working system.
The Build Problem
Building Docker images requires RAM. Building a Next.js app inside a Docker container on a small server requires more RAM than a small server has. I learned this the hard way when docker compose build would just hang indefinitely on a 512MB VPS, the cheapest host option in AWS Lightsail.

I told Claude it was hanging, and it immediately identified the issue: insufficient RAM for the Node.js build process. It gave me a few solutions — add swap space, use a container registry to build elsewhere, or upgrade to a larger VPS. I built the containers remotely using GitLab at first, but I eventually upgraded the VPS instance and added additional swap space so I could build on the VPS.

Environment Variables and Special Characters
I generated a random database password with openssl rand -base64 24. The password contained a + character. That + got embedded in the database URL (postgres://user:pass+word@db:5432/roulette) and broke Python’s URL parser. The error message — ValueError: Port could not be cast to integer value as ‘zu1mnDRpA6v+’ — made no sense to me.

I pasted it into Claude. Within seconds, it identified that the + in the base64 password was being interpreted as a URL-encoded space, corrupting the port parsing. The fix: use openssl rand -hex 24 instead. Only alphanumeric characters. No special characters to break URL parsing.

This is the kind of bug that could take hours to figure out on your own. Claude diagnosed it from the error message in one turn. That’s not replacing my learning — it’s accelerating it. I now understand URL encoding, database connection strings, and why special characters in environment variables are dangerous. I learned it through a real bug, not a textbook.
HTTPS: The Certificate Dance
Getting SSL working was its own adventure. Claude generated the entire Nginx configuration, the Certbot Docker setup, and a setup script that handled the chicken-and-egg problem: Nginx needs SSL certificates to start, but Let’s Encrypt needs Nginx running to verify your domain.

The solution is a multi-step dance:

Create a temporary self-signed certificate so Nginx can boot
Start Nginx
Run Certbot to get a real certificate via the ACME challenge
Reload Nginx with the real certificate

Except the real certificate ended up in bigspinfun.com-0001 instead of bigspinfun.com because the self-signed cert was already occupying the expected directory. So Nginx kept serving the self-signed cert, and browsers showed a security warning.

I told Claude the browser was showing “Not Secure.” It had me check which certificate was actually being served using openssl s_client, which revealed the self-signed cert was still active. Claude figured out that Certbot had created the real cert in a -0001 suffixed directory to avoid overwriting the temp cert. The fix was renaming the directory:

sudo mv …/bigspinfun.com-0001 …/bigspinfun.com

docker compose restart nginx

This was a problem neither of us anticipated. Claude didn’t generate a bug-free setup — it generated a setup that worked in theory but hit an edge case in practice. Then it debugged that edge case from the symptoms I reported. That’s how real development works, with or without AI.
Claude for SEO and Marketing
Once the site was running, the conversation shifted from infrastructure to business. I told Claude I wanted advertising revenue and SEO optimization. It laid out a complete strategy:

SEO first — the site needs to be crawlable before ads make sense
Advertising second — apply for AdSense once content is solid
Expense tracking last — internal tooling can wait

Claude then generated everything for SEO: a robots.txt, a dynamic sitemap, JSON-LD structured data, Open Graph tags, Twitter Cards, and three full content pages targeting specific search queries — “how to play roulette,” “roulette odds,” and “roulette bet types.” Each page had unique metadata, proper heading hierarchy, FAQ sections with schema markup, and internal linking.

It also generated a privacy policy (required for AdSense) and walked me through submitting the sitemap to Google Search Console and Bing Webmaster Tools, setting up Google Analytics, and fixing SEO issues that Bing’s crawler flagged — a missing H1 tag and duplicate canonical URLs.

The SEO work was where Claude’s breadth of knowledge really showed. It wasn’t just writing code — it was making strategic decisions about which keywords to target, how to structure content for featured snippets, how to optimize for AI search engines (allowing GPTBot and ClaudeBot in robots.txt), and when to use server-rendered pages vs client-side components.
What AI-Assisted Development Actually Feels Like
People have strong opinions about AI coding tools. Some think it’s cheating. Some think it produces garbage. Some think it replaces developers. After building an entire production application through conversation with Claude, here’s my experience:

It’s not cheating. I made every architectural decision. I chose the stack, the hosting, the domain, the feature set. Claude implemented those decisions, but the judgment calls were mine. When Claude suggested Next.js for SEO, I had to understand what SSR meant and why it mattered before saying yes.

It’s not garbage. The code Claude generated was clean, well-structured, and production-ready. The Django models had proper indexes and ordering. The Nginx config had security headers and gzip compression. The React components used proper state management. Was it perfect? No — the missing migration files and the SSL certificate edge case prove that. But the quality was consistently high.

It doesn’t replace understanding. I learned more about web infrastructure in this project than in any course I’ve taken. Every error message I pasted into Claude became a learning opportunity. I didn’t just fix the problem — I understood why it happened. The AI compressed what would have been weeks of Stack Overflow and documentation into hours of focused, contextual learning.

It changes the ambition curve. Without Claude, I would have built a static HTML roulette game and called it done. With Claude, I built a full-stack application with user auth, server-side RNG, a PostgreSQL database, Docker containers, Nginx reverse proxy, SSL certificates, SEO optimization, and analytics — and deployed it to AWS. The AI didn’t lower the bar. It raised how far I was willing to reach.

The conversation is the product. The most underrated aspect of AI-assisted development is the conversation itself. Every decision, every debugging session, every “why did we do it this way” is captured in the chat history. It’s like pair programming with someone who has infinite patience and remembers everything you’ve discussed.
GitLab as the Backbone
Throughout all of this — Windows to Rocky to Lightsail — GitLab was the constant. Every change got committed and pushed. When I broke something (which happened regularly), I could look at the git log and figure out what changed. When I moved servers, I could clone the repo and have the entire project structure in seconds.

I set up SSH keys on each server so git operations were seamless. No tokens to manage, no passwords to type. Claude walked me through the SSH key setup for each environment.

The GitLab Container Registry was a bonus I didn’t expect. Free private Docker image hosting, integrated with the repo. Push an image, pull it on any server. No separate Docker Hub account needed.
The Stack Today
The production setup at bigspinfun.com:

Next.js 14 — server-side rendered frontend with SEO content pages
Django REST Framework — user accounts, server-side RNG, game history API
PostgreSQL 16 — persistent storage for users, spins, leaderboard
Nginx — reverse proxy, SSL termination, gzip, security headers
Let’s Encrypt — auto-renewing SSL certificates via Certbot
Docker Compose — all four services defined in one file
AWS Lightsail — Ubuntu instance with 2GB RAM, 12$ per month, at the time of this writing
GitLab — source control and container registry
Claude — development partner from first line of code to production deployment

The entire thing can be cloned and started with docker compose up. That was always the goal — portability. Move it to any server with Docker installed, and it just works.
What I’d Do Differently
If I started over, I’d start by using the cloud from day one. I would start with the cheapest solution and work my way up from there. I’d also set up the migration files and Docker Compose health checks. The number of hours spent debugging “why won’t Django start” because of missing migrations was embarrassing — even with Claude helping.

I’d also start with HTTPS immediately. Setting it up after the fact means reconfiguring CORS origins, cookie settings, and CSRF trusted origins. Setting it up first means everything just works with the secure defaults.

And I’d build on a machine with at least 2GB of RAM from the start. The time spent debugging “why is Docker Compose hanging” on undersized servers was time I could have spent on features.
The Point
This project taught me more about real-world web development than any course or tutorial. Not because the roulette game is complex — it’s a fairly simple application. But because deploying it to production, keeping it running, and making it accessible to the world involves a web of interconnected systems that you can only understand by building through the problems.

Claude didn’t remove the problems. It made them solvable at a pace that kept me engaged instead of frustrated. Every error became a five-minute conversation instead of a two-hour rabbit hole. That’s the difference between giving up and shipping.

If you’re trying to learn infrastructure, pick a project you actually want to build and deploy it for real. Not to localhost. Not to a tutorial sandbox. To a real server with a real domain and real SSL certificates. And if you use an AI to help you get there, don’t feel guilty about it — feel empowered. The understanding you gain from building something real, even with help, is worth more than the theoretical knowledge of doing it alone.

The game is live at bigspinfun.com. It’s free, no real money, no signup required. Spin the wheel, place some bets, and if you’re curious about how it’s built, now you know.

Stop Asking Gemma 4 to Just Summarize

Baseline summary output in Google AI Studio using Gemma 4 26B A4B IT

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

A small test of using open models to expose uncertainty, not hide it.

Most AI demos start with clean prompts.

Real work usually starts with messy notes.

A stakeholder says the number looks wrong. Someone else says a field changed. Another person says the source file was cleaned. The dashboard owner is out. A manager still needs an update before a leadership meeting.

That is the kind of situation I wanted to test with Gemma 4.

I work around business systems, reporting, data quality, and process handoffs, so this felt familiar. In real reporting work, the dangerous moment is not always when the dashboard breaks. It is when everyone wants an answer before anyone has checked the source.

So I tested one practical question:

Can Gemma 4 do something more useful than summarize messy notes?

More specifically:

Can it separate the mess into what is known, what is assumed, what is risky, and what still needs checking?

That is the difference I cared about.

A summary compresses messy information. A review packet separates it into facts, assumptions, risks, and next checks.

The test setup

I kept the test small on purpose.

I used Google AI Studio with Gemma 4 26B A4B IT. I used the same messy notes twice, with the same model settings: temperature 0.25, thinking level set to High, no tools, and no system instructions.

I left system instructions blank because I wanted the prompt itself to carry the behavior.

This was not a model comparison. It was a prompt-pattern comparison.

The only thing I changed was the prompt.

The scenario was synthetic, but realistic: a weekly operations report is due Monday morning, the totals look lower than expected, a field name changed last week, duplicate rows may have been removed, and the dashboard owner is out.

No private data. No company data. Just the kind of messy report situation that creates pressure before a meeting.

Here were the notes, shortened for the test:

A weekly operations report is due Monday morning.

The report usually refreshes Sunday night.

A field name changed last week.

A stakeholder says totals look lower than usual.

Duplicate rows may have been removed from the source file.

The dashboard owner is out.

Nobody knows yet whether the issue is source data, filter logic, or a definition change.

Prompt 1: ask for a normal summary

First, I asked:

Summarize these notes for the manager and explain what is going on.

The result was not bad.

Baseline summary output in Google AI Studio using Gemma 4 26B A4B IT

Baseline run: same Gemma 4 model, same notes, no system instructions.

Gemma 4 gave me a manager-ready update and a plain-English explanation of why the team should avoid giving final numbers yet. One useful line was:

“Investigation in progress regarding a discrepancy in the weekly totals.”

I could imagine sending a version of that in Slack.

But the summary also showed the weakness of summarization. It used phrases like “red alert,” “flying blind,” and “the numbers look wrong.”

Those phrases made the situation easier to understand, but they also added more certainty and drama than the original notes supported.

The raw notes did not prove the report was wrong. They only said the totals looked lower than usual.

That matters.

A summary can make messy information easier to read while quietly making the situation feel more settled than it actually is.

Prompt 2: ask for a review packet

So I reran the same notes with one change: instead of asking for a summary, I asked for a review packet.

You are an analyst preparing a review packet.

Using only the notes below, return:
1. One-paragraph summary
2. Confirmed facts
3. Assumptions
4. Unverified claims
5. Risks
6. Questions for a human reviewer
7. Suggested next actions
8. What not to conclude yet
9. Final checklist

Rules:
- Do not invent facts.
- Separate facts from assumptions.
- Keep it calm, practical, and concise.
- Help a human decide what to verify next; do not make the final decision.

The output was less polished, but more useful for the decision in front of the team.

It separated confirmed facts from assumptions and unverified claims. For example, it treated “totals look lower than usual” as an unverified claim because that came from a stakeholder report, not from a completed data check.

It also kept the biggest unknown visible:

“The root cause is currently unknown.”

That was the most important part of the output.

In a real reporting issue, people often want to jump straight to the cause: the field change broke it, the source file changed it, the filter logic is wrong.

But in this scenario, none of that had been verified yet.

The strongest section was “What not to conclude yet”:

  • Do not conclude that the data is incorrect.
  • Do not conclude that the field name change or duplicate removal caused the discrepancy.
  • Do not conclude whether the issue is source data, filter logic, or a definition change.

That section changed the output from a status update into a review tool.

Review packet output in Google AI Studio separating confirmed facts, assumptions, unverified claims, and risks

Review-packet run: the useful shift was separating facts, assumptions, risks, and what not to conclude yet.

What changed

Summary vs Review Packet comparison

The better output was not better because it sounded smarter.

It was better because it gave the team a safer shape for the next step.

The summary was better for status communication. The review packet was better for deciding what still needed verification.

The caution: this still does not verify anything

This is the boundary I would not cross.

Gemma 4 did not inspect the report. It did not open the source file. It did not check the dashboard logic. It did not know whether duplicate removal was correct or whether the field name change affected a calculation.

It organized the review path.

That is useful, but it is not the same as verification.

In a real report issue, I would still check the source rows, field mapping, report filters, refresh timing, and metric definition before saying anything final. The review packet does not remove that work. It makes sure I do not skip it.

For messy systems work, I would use this kind of output before the investigation, not after it. It is a way to prepare the human reviewer, not replace them.

The model can turn scattered notes into a checklist for the person doing the investigation.

It cannot do the investigation unless it has access to the actual systems, data, definitions, and business context.

The Review Packet Framework

If someone only copied one thing from this post, I would want it to be this prompt shape.

When the input is messy, uncertain, or time-sensitive, ask the model for:

  1. Summary
  2. Confirmed facts
  3. Assumptions
  4. Unverified claims
  5. Risks
  6. Questions for a human reviewer
  7. Suggested next actions
  8. What not to conclude yet
  9. Final checklist

The most useful parts are not always the summary or the checklist.

For me, the important sections are:

  • assumptions
  • unverified claims
  • risks
  • what not to conclude yet

Those sections force the model to keep uncertainty visible instead of hiding it inside a clean paragraph.

That is what made the review packet more useful than the summary.

Why I used Gemma 4 this way

I used Gemma 4 this way because messy notes are a real workload.

Reports, support handoffs, incident updates, and project notes rarely arrive in perfect form. The useful question is not only whether a model can write about them. It is whether it can preserve the uncertainty inside them.

That fits the part of Gemma 4 I care about most: not just text generation, but reasoning over messy context and producing something structured enough for a human to use.

For this test, I used the 26B A4B instruction model because I cared more about the reasoning structure than running the smallest possible model. If I were building a lightweight offline helper, I would test a smaller Gemma 4 model next.

Final takeaway

I would still ask Gemma 4 for summaries.

But after this test, I would be more careful about when a summary is enough.

If the situation is messy, time-sensitive, or full of unverified claims, I would rather ask for a review packet.

The useful output is not always the cleanest paragraph. Sometimes it is the output that says:

  • here is what we know
  • here is what we are assuming
  • here is what could go wrong
  • here is what not to conclude yet

That is the pattern I would reuse.

Gemma 4 did not remove the mess.

It made the mess reviewable.