Hiring engineers in the age of AI

Your engineering interview loop was designed for a world where the bottleneck was writing code. The best engineers in 2026 are not the fastest coders — they’re the ones with the best judgment about what to build, how to direct AI toward the right outcome, and when to override what it produced.

The job title still says “engineer,” but the role has fractured. The person you actually need is part architect, part product thinker, part operator, and part team multiplier — designs systems before touching a keyboard, kills bad ideas before they become features, plans for the 3am page before it fires, makes the people around them better. AI writes the code. The human decides whether the code should exist at all.

Augment Code put it well in their hiring framework: the human role has shifted from author to architect and editor. You define intent, make trade-off decisions, set guardrails, and serve as the last line of quality. Raw coding ability no longer separates exceptional engineers from competent ones — regardless of whether they’re writing backends, frontends, data pipelines, or infrastructure.

CoderPad’s 2026 State of Tech Hiring confirms this from the demand side — technical assessments are up 48% globally, and 82% of developers find AI at least somewhat useful. Companies leading in AI are hiring more engineers, not fewer. The bottleneck isn’t code generation. It’s judgment, systems thinking, and the ability to work alongside AI without losing control of the outcome.

If your interview loop still centers on “can this person write a balanced binary tree on a whiteboard,” you’re selecting for a skill that AI does better than any human. Here’s what to select for instead.

The rubric

Six dimensions, each with sub-criteria you score independently. 3 = strong, 2 = adequate, 1 = weak. Open the scorecard (Google Sheet).

1. Architecture & design

Can the candidate think at the level of systems, not just functions? These apply whether someone is building APIs, frontends, data pipelines, or infrastructure.

  • Trade-off analysis under ambiguity — can they explain why one approach beats another in terms the business cares about, not just technical preference?
  • Failure mode awareness — do they design for the 3am outage, or only the happy path?
  • Scale reasoning — do they ask about constraints (current bottlenecks, data patterns, cost) before proposing solutions?
  • API and contract design — can they design interfaces that other teams can integrate safely?
  • Data modeling and storage decisions — do they choose the right storage for the access pattern, not just the storage they know?

2. AI-assisted execution

Can the candidate direct AI tools toward the right outcome — and catch it when the output is wrong?

  • Directs AI toward well-defined subtasks — uses AI for implementation while keeping control of the overall design. Treats AI as a tool, not an oracle.
  • Critically reviews and debugs AI output — can explain why AI-generated code is or isn’t correct. This is the single strongest signal.
  • Clear boundary between AI-delegated and human-owned work — knows which tasks to hand off and which to protect. No boundary means no judgment.
  • Understands AI limitations in production contexts — knows where models hallucinate, where context windows break, where AI-generated code introduces security or concurrency risks.

3. Systems & operations

Does the candidate understand what makes code production-worthy?

  • Observability — thinks about monitoring, alerting, and SLOs from day one. Monitors what the user cares about, not what the default dashboard provides.
  • Debugging methodology — has a systematic approach: logs, metrics, traces, reproduction. Asks what changed recently before guessing.
  • Cost awareness — considers compute, storage, and API call costs as design constraints, not afterthoughts.
  • Incident response and on-call mindset — has a mental model for what happens after the code is merged. Designs for operability.

4. Product & problem selection

Does the candidate solve the right problems, or just the ones assigned?

  • Asks about users and business context before building — wants to know the success metric, not just the spec.
  • Pushes back on problem framing when appropriate — would rather solve a simpler problem well than an interesting problem poorly.
  • Chooses simple solutions over interesting ones — the boring choice that saves the team real time or pain.

5. Learning velocity

How fast does the candidate adapt when tools and practices change underneath them?

  • Has adopted and discarded tools based on results — not just tried things, but evaluated and made deliberate choices.
  • Can articulate what changed in their workflow and why — adoption without displacement usually means the tool isn’t actually being used.
  • Stays current without hype-chasing — runs personal experiments but doesn’t chase every new release.

6. Culture & collaboration

AI amplifies individual output — but a 10x individual who can’t collaborate is a net negative. Score this separately from technical ability.

  • Curiosity — asks questions, explores unfamiliar territory, wants to understand the system beyond their immediate scope.
  • Navigates disagreement constructively — describes what they learned from technical disagreements, not just how they won them.
  • Communicates technical decisions to non-engineers — can explain trade-offs in terms a PM or exec can act on.
  • Shares context on AI-generated code with the team — flags AI-generated sections in PRs, documents reasoning, takes responsibility for the review burden.
  • Mentors or lifts others — impact isn’t just individual output. Improves the people around them.
  • Holds opinions loosely — has strong technical convictions but adapts to team norms and new information.

The questions

Fourteen questions, grouped by what they assess. Each one includes what you’re listening for and the red flag.

Architecture & design

1. “Walk me through a system you designed that had to handle a non-obvious failure mode. What was the failure, and how did your design account for it?”

Listen for: specifics about the failure mode, the trade-off they made, and whether they designed for it proactively or reactively. Strong candidates talk about the failure before it happened.

Red flag: only discusses happy-path architecture.

2. “You’re designing a service that needs to process 10x its current load within 6 months. Where do you start?”

Listen for: questions before answers. Strong candidates ask about current bottlenecks, data access patterns, and cost constraints before proposing solutions. They think about what not to change as much as what to change.

Red flag: immediately proposes a technology (“just use Kafka”) without understanding the constraints.

3. “Tell me about a time you chose a boring solution over an interesting one. Why?”

Listen for: a clear trade-off between technical elegance and operational simplicity. The best answer is a story where the boring choice saved the team real time or pain.

Red flag: can’t think of one. Engineers who always choose the interesting solution are expensive to operate alongside.

AI-assisted execution

4. “Walk me through your daily workflow. Where does AI fit in, and where doesn’t it?”

Listen for: specificity. Strong candidates can name exact tools, describe which tasks they delegate to AI, and — critically — which tasks they don’t. The boundary matters more than the tools.

Red flag: vague answers like “I use Copilot for everything.” No boundary means no judgment.

5. “Tell me about a time AI-generated code was wrong in a way that wasn’t obvious. How did you catch it?”

Listen for: a real story with a real failure mode. Strong candidates describe a subtle bug — a race condition, a missed edge case, a security hole — that they caught through review, testing, or domain knowledge. This is the single strongest signal for AI-assisted work: the ability to be the quality backstop.

Red flag: “that hasn’t really happened to me.” It has. They just didn’t notice.

6. “If I gave you a new codebase you’ve never seen and asked you to add a feature using AI tools, how would you approach it?”

Listen for: a process that starts with understanding, not prompting. Strong candidates talk about reading the existing code, understanding the architecture, then using AI for implementation — not pasting requirements into a chat window and hoping.

Red flag: leads with “I’d prompt the AI to…” without any mention of understanding the system first.

Systems & operations

7. “Your service is throwing 500s for 2% of requests. Walk me through your debugging process.”

Listen for: a systematic approach — logs, metrics, traces, reproduction. Strong candidates ask what changed recently, check deployment history, and think about partial failures. Let them use AI tools during the discussion. Watch whether they use AI to accelerate diagnosis or to replace thinking.

Red flag: guesses without data. “It’s probably the database” is not a debugging process.

8. “How do you decide what to monitor in a new service?”

Listen for: a framework, not a checklist. Strong candidates think about SLOs, user-facing metrics, and leading indicators of failure — not just CPU and memory. They monitor the thing the user cares about, not the thing the infrastructure provides by default.

Red flag: “whatever the default dashboard gives us.”

Product & problem selection

9. “Tell me about a feature you decided not to build. What was the reasoning?”

Listen for: business judgment. The feature was technically feasible but wrong for the user, the timeline, or the system’s current maturity. Strong candidates kill their own ideas.

Red flag: can’t think of a feature they chose not to build. This suggests they build whatever’s asked without filtering.

10. “A PM asks you to add a feature that you think is a bad idea. What do you do?”

Listen for: pushback with evidence, not ego. Strong candidates describe how they’d frame the concern — data, user impact, operational cost — and what they’d do if overruled. The answer to “what if they still want it” matters as much as the initial pushback.

Red flag: “I’d just build it, they’re the PM.” Or the opposite: “I’d refuse.” Neither is a real answer.

Learning velocity

11. “What AI tool or workflow did you adopt in the last 6 months that meaningfully changed how you work? What did it replace?”

Listen for: a concrete change in behavior, not a tool name. Strong candidates describe what they stopped doing when they adopted the new thing. Adoption without displacement usually means the tool isn’t actually being used.

Red flag: names a tool but can’t describe the workflow change. “I started using Cursor” is not an answer. “I stopped writing boilerplate tests by hand because Cursor generates them and I review them” is.

12. “What’s something AI tools are bad at today that people assume they’re good at?”

Listen for: nuance and firsthand experience. Strong candidates have bumped into real limitations — context window issues, hallucinated APIs, broken concurrency patterns — and can describe them specifically. This question reveals whether someone has used AI tools enough to know where they break.

Red flag: “AI is pretty good at everything now.” It is not.

Culture & collaboration

13. “Tell me about a technical disagreement you had with a teammate. How did it resolve, and would you do anything differently?”

Listen for: respect for the other person’s view, willingness to change their mind, and a resolution that wasn’t just “I was right.” Strong candidates describe what they learned, not just how they won.

Red flag: every story ends with them being right. Or they can’t recall a disagreement — which means they either avoid conflict or don’t notice it.

14. “How do you share context with your team when you’ve used AI to generate a significant chunk of code?”

Listen for: a process — PR descriptions that call out AI-generated sections, documentation of the reasoning, flagging areas that need extra review. Strong candidates recognize that AI-generated code shifts the review burden to the team and own that.

Red flag: “I just push it like any other code.” That’s how AI-generated bugs become the team’s problem.

Red flags to watch across the loop

A few patterns that span the whole loop:

Tool fluency without technical depth. The candidate is fast with AI tools but can’t explain the code they produce. This is the most dangerous hire in 2026 — someone who ships faster but embeds hidden defects. Canva’s interview team found the same thing: the best candidates don’t just prompt — they ask clarifying questions, use AI for well-defined subtasks, and critically review output.

No boundary between AI tasks and human tasks. Every engineer needs a clear mental model of what they delegate and what they don’t. If someone uses AI for everything indiscriminately, they’ve abdicated judgment.

Resistance to AI as identity. Some candidates treat not using AI as a badge. In 2026, this is like refusing to use an IDE. It doesn’t demonstrate skill — it demonstrates rigidity.

Can’t debug AI output. If you ask them to review AI-generated code and they can’t identify issues, they’re a liability in a codebase where AI writes the first draft.

The heuristic

You’re not hiring a coder anymore. The engineer you need in 2026 is an architect who designs systems before writing them, a product thinker who kills bad ideas before they become tickets, an operator who plans for the 3am page, and a teammate who makes everyone around them better. AI handles the typing. You’re hiring for everything else. Interview for judgment, not keystrokes — the candidate who pushes back on AI output is worth more than the one who prompts their way to a solution.

tl;dr

The pattern. Engineering interviews still optimize for raw coding ability — a skill AI now handles — while ignoring the architect, product thinker, operator, and collaborator the role actually demands.
The fix. Score candidates across six dimensions (architecture, AI-assisted execution, systems & ops, product taste, learning velocity, culture) with specific sub-criteria, and use questions that reveal how they direct, evaluate, and override AI output.
The outcome. You hire engineers who think like architects, challenge like PMs, operate like SREs, and use AI as a tool — not engineers who either ignore it or depend on it blindly.

Originally published at b0gy.com

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.