Definition
- Bug life cycle – The sequence of states a defect moves through from discovery to closure: New, Assigned, Open, Fixed, Retest, Verified, and Closed.
- Alternate states – Handle non-linear outcomes: Reopened when a fixed bug recurs, Rejected when it is not a valid defect, Duplicate when it matches an existing bug, Deferred when postponed, and Not Reproducible when the developer cannot recreate it.
- Cannot-reproduce handling – Supply exact environment, build version, test data, reproduction steps, logs, screen recordings, and network traces to eliminate the context gap between tester and developer.
- Minimal deterministic repro – The smallest set of steps and data that reliably triggers the failure every time, which is the key to converting a vague report into a fixable bug.
When a teller at the National Bank of India spots a problem, say the balance screen shows a wrong figure, it starts as a New complaint. It is handed to an engineer, Assigned, who starts work, Open, makes the repair, Fixed, hands it back for the teller to re-check, Retest, the teller confirms it is correct, Verified, and the ticket is filed away, Closed. Sometimes the path bends: the problem comes back, Reopened; the engineer says it is working as designed, Rejected; someone already reported it, Duplicate; it is real but will wait until next release, Deferred; or the engineer simply cannot make it happen on their machine, Not Reproducible. For that last case, the teller must hand over the exact branch, the app version, the specific account and amounts, the precise click-by-click steps, screen recordings, and the system logs, so the engineer can finally see it happen.
Understanding the Concepts
What is the bug life cycle?
The bug life cycle is the sequence of states a defect moves through from discovery to closure. Each state has a clear owner and a clear purpose.
- New : The tester logs the defect with a clear title, steps to reproduce, expected and actual results, and evidence.
- Assigned : A triage lead or manager assigns the bug to a specific developer.
- Open : The developer begins investigating and working on the fix.
- Fixed : The developer commits the fix and the build carrying that fix is deployed to a test environment.
- Retest : The tester re-runs the exact reproduction scenario against the new build.
- Verified : The tester confirms the fix works and nothing else broke.
- Closed : The defect is fully resolved and needs no further action.
// Bug state machine in code
package com.bank.qa;
public enum BugState {
NEW, ASSIGNED, OPEN, FIXED, RETEST, VERIFIED, CLOSED,
// Alternate states
REOPENED, REJECTED, DUPLICATE, DEFERRED, NOT_REPRODUCIBLE
}
public class BugLifecycle {
private BugState state = BugState.NEW;
public void assign(String developer) {
if (state != BugState.NEW && state != BugState.REOPENED) {
throw new IllegalStateException(
"Can only assign from NEW or REOPENED, current: " + state
);
}
this.state = BugState.ASSIGNED;
}
public void startWork() {
if (state != BugState.ASSIGNED) {
throw new IllegalStateException("Must be ASSIGNED first");
}
this.state = BugState.OPEN;
}
public void markFixed(String commitId) {
if (state != BugState.OPEN) {
throw new IllegalStateException("Must be OPEN to fix");
}
this.state = BugState.FIXED;
}
public void retest() { this.state = BugState.RETEST; }
public void verify() { this.state = BugState.VERIFIED; }
public void close() { this.state = BugState.CLOSED; }
public void reopen() { this.state = BugState.REOPENED; }
}
What are the alternate states?
Alternate states handle real-world non-linearity in the defect resolution process.
- Reopened : A Fixed-and-closed bug recurs, often because the fix was incomplete or a regression reintroduced it. The tester reopens rather than filing a new bug so history is preserved.
- Rejected : The reported behavior is actually correct per the requirements or is a misunderstanding. The developer explains why so the tester can learn or escalate.
- Duplicate : The defect is the same underlying issue as an existing one. You link the two so the fix on the original resolves both.
- Deferred : A valid bug that the business consciously postpones, typically because it is low priority and the release cannot wait.
- Not Reproducible : The developer cannot recreate the failure, which triggers the context-gathering discipline.
// Testing alternate state transitions
package com.bank.qa;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class BugLifecycleTest {
@Test
void reopenedBugCanBeReassigned() {
BugLifecycle bug = new BugLifecycle();
bug.assign("Rajesh");
bug.startWork();
bug.markFixed("abc123");
bug.retest();
bug.verify();
bug.close();
bug.reopen(); // Bug came back
bug.assign("Rajesh"); // Re-assign, preserving history
assertEquals(BugState.ASSIGNED, bug.getState());
}
@Test
void cannotAssignFromFixedState() {
BugLifecycle bug = new BugLifecycle();
bug.assign("Rajesh");
bug.startWork();
bug.markFixed("abc123");
assertThrows(IllegalStateException.class, () ->
bug.assign("Priya")
);
}
}
How do you handle a "cannot reproduce" bug?
Handling a cannot-reproduce bug well is a senior skill because non-reproducibility is usually a symptom of missing context, not a non-existent bug. The disciplined response is to gather and attach everything that could differ between the tester's and developer's environments.
The checklist for a cannot-reproduce response:
- Exact environment : Operating system, browser and version, device, deployed environment (staging vs QA).
- Exact build version : Application version, commit hash, dependency versions.
- Exact data : Specific account identifiers, amounts, special states such as a dormant account or one over a limit.
- Exact steps : Precise ordered steps with no assumed knowledge, click-by-click.
- Rich evidence : Screen recording at the moment of failure, application and server logs correlated by request or trace ID, browser network trace or HAR file.
# Capturing a HAR file for network evidence
# In Playwright, record HAR automatically
npx playwright test --grep "failing-test" -- --har=evidence/bug-4521.har
# In Chrome DevTools:
# Network tab -> right-click -> Save all as HAR with content
// Bug report with complete reproduction context
package com.bank.qa;
public class CannotReproduceBugReport {
String bugId = "BUG-4521";
String title = "Debit fails for dormant accounts over 180 days";
// Environment
String os = "macOS 14.2";
String browser = "Chrome 121.0.6167.85";
String environment = "staging-2";
String appVersion = "v3.14.2 (commit abc123)";
// Data
String accountId = "ACC-78901";
String accountState = "DORMANT (inactive 185 days)";
String amount = "5000.00";
// Steps
String[] steps = {
"1. Log in as teller user T-001",
"2. Navigate to /accounts/ACC-78901",
"3. Click 'Debit' button",
"4. Enter amount 5000.00",
"5. Click 'Confirm'",
"6. ACTUAL: Error 500 'NullPointerException'",
"7. EXPECTED: Debit processed or clear rejection message"
};
// Evidence
String screenRecording = "evidence/bug-4521-recording.mp4";
String serverLog = "evidence/bug-4521-server.log";
String traceId = "trace-abc-123-def-456";
String harFile = "evidence/bug-4521.har";
}
What is a minimal deterministic repro?
The goal is to reduce the problem to the smallest set of steps and data that reliably triggers the failure every time. Determinism is what converts a hand-waving "sometimes it breaks" into a bug a developer can fix.
// Minimal repro: distilled to the smallest failing case
package com.bank.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class DormantAccountDebitReproTest {
@Test
void debitFailsForAccountDormantOver180Days() {
// Minimal data: just the dormant state matters
Account account = new Account("ACC-TEST",
new java.math.BigDecimal("50000.00"));
account.setLastActivityDate(
java.time.LocalDate.now().minusDays(185)
);
account.setStatus(AccountStatus.DORMANT);
DebitService service = new DebitService();
// This is the exact failure: NPE because dormant
// accounts have null reactivation date
assertThrows(NullPointerException.class, () ->
service.debit(account, new java.math.BigDecimal("5000.00"))
);
}
}
Original vague report:
"Sometimes debits fail for some accounts"
Minimal deterministic repro:
"Debit fails with NullPointerException for any account
dormant over 180 days, because the reactivation date
field is null and the debit service does not check
for null before calling .isAfter()"
What are common hidden causes of non-reproducibility?
Non-reproducibility often exposes environmental or data differences that are genuine bugs lurking in specific conditions.
- Environment differences : Different OS, browser version, or JVM version between tester and developer.
- Time zone or locale : A bug that only appears in IST but not UTC, or with Indian locale date formatting.
- Specific data states : An account that is dormant, frozen, over a limit, or has zero transactions.
- Race conditions and timing : A bug that appears only under load or when two requests overlap.
- Cached state : The developer's environment has cached data that masks the bug.
- Feature flags : A flag enabled in staging but not in the developer's local environment.
- Authentication differences : A permission or role difference between the tester's and developer's users.
// Race condition example: only fails under concurrency
package com.bank.service;
import org.junit.jupiter.api.Test;
import java.util.concurrent.*;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RaceConditionReproTest {
@Test
void concurrentTransfersRevealRaceCondition() throws Exception {
Account shared = new Account("ACC-001", new BigDecimal("10000.00"));
TransferService service = new TransferService();
ExecutorService executor = Executors.newFixedThreadPool(10);
// Submit 10 concurrent debits of 1000 each
var futures = new java.util.ArrayList<Future<?>>();
for (int i = 0; i < 10; i++) {
futures.add(executor.submit(() ->
service.debit(shared, new BigDecimal("1000.00"))
));
}
for (var f : futures) f.get();
executor.shutdown();
// Without synchronization, balance may not be 0
assertEquals(new BigDecimal("0.00"), shared.getBalance());
}
}
What should a senior engineer know?
A senior engineer treats Not Reproducible as a collaboration prompt rather than a dead end. The response is never "I cannot see it, so it does not exist." Instead it is "I cannot see it yet, so I need more context."
- Instrument and wait : If a bug still cannot be reproduced after full context is provided, add extra logging with correlation identifiers and wait for it to recur in production. The next occurrence is captured with everything needed.
- Pair debugging : The tester and developer sit together and reproduce the bug on the tester's machine, which often reveals the environment or data difference immediately.
- Flaky bug tracking : Some bugs are genuinely intermittent due to race conditions or timing. Track them separately and investigate with concurrency tests, not by closing them as Not Reproducible.
// Instrumentation for a bug that cannot be reproduced
package com.bank.service;
import java.util.logging.Logger;
public class DebitService {
private static final Logger log =
Logger.getLogger(DebitService.class.getName());
public void debit(Account account, java.math.BigDecimal amount) {
// Extra logging for BUG-4521 investigation
log.info(String.format(
"[BUG-4521] debit called: account=%s status=%s "
+ "lastActivity=%s reactivationDate=%s amount=%s",
account.getId(), account.getStatus(),
account.getLastActivityDate(),
account.getReactivationDate(), amount
));
if (account.getReactivationDate() == null) {
log.severe("[BUG-4521] reactivationDate is null!");
// Now we will capture this in production logs
}
// ... rest of debit logic
}
}
What are common interview follow-up questions?
- "What if the developer closes a bug as Not Reproducible and you disagree?" Provide the full context checklist. If it still cannot be reproduced, suggest pair debugging or adding instrumentation.
- "When is it acceptable to close a bug as Not Reproducible?" Only after the full context has been provided, pair debugging attempted, and instrumentation added for future occurrences. Never on the first attempt.
- "How do you track the cost of a Not Reproducible bug?" Count the round-trips between tester and developer. Each round-trip is wasted time. A complete first report with all context minimizes round-trips.
How do you write a complete bug report?
A complete bug report minimizes round-trips between tester and developer.
// Bug report template as code
package com.bank.qa;
public class BugReportTemplate {
String id = "BUG-4521";
String title = "Debit fails for dormant accounts over 180 days";
String severity = "CRITICAL";
String priority = null; // Set in triage
String environment = "staging-2, Chrome 121, macOS 14.2";
String appVersion = "v3.14.2 (commit abc123)";
String[] stepsToReproduce = {
"1. Log in as teller T-001",
"2. Navigate to /accounts/ACC-78901",
"3. Click Debit button",
"4. Enter amount 5000.00",
"5. Click Confirm",
};
String expectedResult = "Debit processed or clear rejection";
String actualResult = "Error 500 NullPointerException";
String[] evidence = {
"screenshot: bug-4521-screenshot.png",
"video: bug-4521-recording.mp4",
"trace: trace-abc-123-def-456",
"server-log: bug-4521-server.log",
};
String workaround = "Use accounts active within 180 days";
int affectedUsers = 1200; // Dormant account holders
}
Visual Overview
Bug Life Cycle: Main Flow
─────────────────────────
┌─────┐ ┌──────────┐ ┌──────┐ ┌───────┐
│ NEW │───>│ ASSIGNED │───>│ OPEN │───>│ FIXED │
└─────┘ └──────────┘ └──────┘ └───┬───┘
│
▼
┌────────┐ ┌──────────┐ ┌─────────┐
│ CLOSED │<───│ VERIFIED │<───│ RETEST │
└────────┘ └──────────┘ └─────────┘
Alternate States
────────────────
CLOSED ──────> REOPENED ──────> ASSIGNED
(bug recurs) (preserves history)
NEW ──────> REJECTED
(not a bug / working as designed)
NEW ──────> DUPLICATE
(linked to existing bug)
NEW ──────> DEFERRED
(valid but postponed to later release)
OPEN ─────> NOT REPRODUCIBLE
(developer cannot recreate)
│
▼
Tester provides:
- Exact environment
- Exact version
- Exact data
- Exact steps
- Logs + video + HAR
- Minimal deterministic repro
│
▼
OPEN (developer retries with full context)
Cannot-Reproduce Checklist ────────────────────────── ┌─────────────────────────────────────────┐ │ 1. Environment: OS, browser, version │ │ 2. Build: app version, commit hash │ │ 3. Data: account ID, state, amounts │ │ 4. Steps: precise, ordered, no gaps │ │ 5. Logs: server + app, correlated by │ │ trace ID │ │ 6. Video: screen recording at failure │ │ 7. Network: HAR file with req/response │ │ 8. Minimal repro: smallest failing set │ └─────────────────────────────────────────┘
Key Takeaway
The bug life cycle is the set of states a defect moves through from discovery to closure. It starts at New when I log it with clear steps, expected and actual results, and evidence; moves to Assigned when a developer owns it; to Open while they investigate; to Fixed when the code change lands; to Retest when I re-run the scenario against the new build; to Verified when I confirm the fix and that nothing else broke; and finally to Closed. Around that main line are the alternate states: Reopened when a closed bug recurs, preserving history instead of filing a new one; Rejected when the behavior is actually correct; Duplicate when it matches an existing defect; Deferred when a valid bug is consciously postponed; and Not Reproducible when the developer cannot recreate it.
Handling a cannot-reproduce bug is where I show seniority, because non-reproducibility almost always means missing context rather than a non-existent bug. My response is to supply everything that could differ between my world and the developer's: the exact environment, the exact application build, the exact data including specific account identifiers and special states, the precise ordered steps, and rich evidence including a screen recording, correlated server logs, and a HAR network trace. My goal is a minimal deterministic reproduction, the smallest steps and data that reliably trigger the failure.
At the National Bank of India we once had an intermittent debit failure that a developer marked Not Reproducible. I reproduced it only after noticing it happened only on accounts dormant for more than one hundred eighty days, so I attached that data state, the exact staging build, click-by-click steps, a screen recording, and a server log with the matching trace identifier. The developer fixed it within the day.
I treat Not Reproducible as a collaboration prompt, not a dead end. If the full context still does not reproduce it, I instrument the code with extra logging keyed by a correlation identifier and wait for the next occurrence.
Remember one sentence: a "cannot reproduce" bug is almost always a missing-context bug, so supply the exact environment, data, steps, and evidence until it becomes deterministic.
---