Articles
GitHub Software Engineer Interview Guide 2026: Take-Home, Pair Programming, Code Review, and System Design
Prepare for GitHub engineering interviews with a runnable PR review, permission and lost-update bugs, tested repair, pairing extension and system-design drills.
A useful code review can be only three comments long. One prevents a permissions violation, another protects an author's newer edit, and the third removes a distracting unused import. The skill is identifying which two must stop the change and explaining how the author can prove the repair.
GitHub engineering preparation should include reviewable implementation, collaboration, project judgment and role-relevant design. This guide provides a complete local PR simulation instead of implying that every candidate receives the same interview. The code, repository data, users and severity labels are original teaching material, not GitHub source or a leaked exercise.
For a communication drill, use How do you conduct a code review exercise?. That Airbnb-tagged record practices written, prioritized feedback with limited context. It trains a transferable skill; it does not establish GitHub's scheduled rounds.
Current hiring guidance versus historical technical formats
GitHub's current Experienced Professionals page describes a talent conversation, hiring-manager conversation, possible tool-based assessment and interviews with Hubbers. Coding or writing assessments depend on the role. It emphasizes explaining decisions, individual contributions and impact, and directs candidates to the recruiter for feedback and timing. It does not promise one round count or one technical format.
The official 2022 take-home article documents repository-based work, a pull request, time limits, automated tests and anonymized engineer review. It describes that process at that time; it is not proof that every 2026 team uses Interview-bot. The Codespaces article describes a team's pairing setup, likewise providing context rather than a company-wide mandate.
Before preparing a take-home or live session, confirm the medium, language, time budget, submission format and assistance policy. Historical permission to consult documentation is not current permission to use AI coding tools. Use your invitation for that distinction and the recruiter for unresolved questions.
Original PR exercise: rename an issue safely
You are reviewing a small issue-store change. The request may rename an issue only when the authenticated actor has write or admin permission on the issue's repository. A route contains both repository ID and issue ID; the issue must actually belong to that repository. The caller supplies the version it read, and a stale update must leave newer work intact.
Authentication happens outside the store. actor comes from trusted server context, never an editable request field. The exercise must not imply that accepting a username string authenticates someone. Missing and unauthorized resources share one outward error so the operation does not reveal private issue details.
Use Python's standard sqlite3 module and this synthetic setup. The function owns its transaction; callers must finish setup and commit before invoking it. No HTTP server, GitHub API, package installation or real credentials are needed.
CREATE TABLE issues (
id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL,
title TEXT NOT NULL, version INTEGER NOT NULL
);
CREATE TABLE members (
repo_id INTEGER NOT NULL, actor TEXT NOT NULL, role TEXT NOT NULL,
PRIMARY KEY(repo_id,actor)
);
CREATE TABLE audit (
issue_id INTEGER NOT NULL, repo_id INTEGER NOT NULL,
actor TEXT NOT NULL, old_version INTEGER NOT NULL,
new_version INTEGER NOT NULL
);
INSERT INTO issues VALUES (10,1,'Fix login',1),(20,2,'Private roadmap',1);
INSERT INTO members VALUES (1,'alice','write'),(1,'bob','read');
The proposed patch replaces a stub:
--- a/issue_store.py
+++ b/issue_store.py
@@ -1,2 +1,11 @@
+import json # unused
+
def rename_issue(db, actor, repo_id, issue_id, expected_version, title):
- raise NotImplementedError
+ x = db.execute("SELECT version FROM issues WHERE id=?",
+ (issue_id,)).fetchone()
+ if x is None:
+ return 404, None
+ with db:
+ db.execute("UPDATE issues SET title=?, version=version+1 WHERE id=?",
+ (title, issue_id))
+ return 200, x[0] + 1
Read intent first. This patch uses parameterized SQL, which is useful, but parameterization does not enforce authorization. It accepts actor, repo_id and expected_version without using any of them to constrain the write. Incrementing a version after an update also does not prevent overwriting a newer title.
Reproduce the two blockers before discussing style
For permission scope, give Alice a request for repository 1 and issue 20. Issue 20 belongs to repository 2, where Alice has no membership. The proposed code still changes its title because it selects and updates by issue ID alone. Bob can also rename issue 10 despite having only read permission. Both violate the stated contract.
For a lost update, let two editor views read issue 10 at version 1. The first writer changes the title and advances to version 2. The second writer still submits expected version 1. The proposed code accepts it, overwrites the title and advances to version 3. A correct result rejects that stale second request and keeps the first title at version 2.
The local reproduction runs only against the synthetic fixture. Its expected behavior checks fail on the proposed patch: unauthorized state changes occur, and stale edits are accepted. A passing happy-path test does not resolve either defect.
Three comments an author can act on
Blocker — repository write authorization: “The query matches only issue_id, so Alice's repo-1 request can modify issue 20 in repo 2, and a read-only member can modify issue 10. Please check the issue's repository and the authenticated actor's current write/admin membership in the same transactional operation. Add wrong-repository, nonmember and read-only tests that assert no title or version change.”
Blocker — stale updates: “expected_version is unused. Two clients reading version 1 can both write, and the later request erases the first edit. Please condition the update on the expected version and return a conflict without mutation when it differs. Add a two-writer test with one successful update, one conflict and one final version increment.”
Non-blocking cleanup: “The new json import is unused, and x obscures that the value is the stored version. Please remove the import and use a descriptive name when touching this function. This cleanup is secondary to the two behavior fixes.”
These labels are this exercise's review decisions, not a private GitHub severity system. The summary should request changes because the stated permission and stale-update contracts fail. Do not drown the author in naming suggestions while leaving those defects unaddressed.
Sarah Vessels's public GitHub review article stresses making blockers and personal preferences clear, offering specific evidence and asking questions about assumptions. Apply that principle even if the author is senior or the patch is short. The review should be proportionate to the risk, not the author's reputation.
Corrected patch, including the pairing extension
The repaired operation validates inputs, opens a SQLite write transaction, checks repository membership, compares the expected version and performs a guarded update. The final version below also includes the next pairing requirement: append an audit record in the same transaction.
class NotFound(Exception):
pass
class Conflict(Exception):
pass
def rename_issue(db, actor, repo_id, issue_id, expected_version, title):
if any(type(v) is not int or v <= 0 for v in (repo_id, issue_id)):
raise ValueError("invalid identity")
if type(expected_version) is not int or expected_version < 1:
raise ValueError("invalid version")
if type(title) is not str or not title.strip() or len(title.strip()) > 80:
raise ValueError("invalid title")
if type(actor) is not str or not actor:
raise ValueError("invalid authenticated actor")
if db.in_transaction:
raise ValueError("caller must not hold a transaction")
title = title.strip()
db.execute("BEGIN IMMEDIATE")
try:
row = db.execute("""
SELECT i.version FROM issues AS i
JOIN members AS m ON m.repo_id=i.repo_id
WHERE i.id=? AND i.repo_id=? AND m.actor=?
AND m.role IN ('write','admin')
""", (issue_id, repo_id, actor)).fetchone()
if row is None:
raise NotFound("not found or not allowed")
if row[0] != expected_version:
raise Conflict("stale version")
changed = db.execute("""
UPDATE issues SET title=?, version=version+1
WHERE id=? AND repo_id=? AND version=?
""", (title, issue_id, repo_id, expected_version))
if changed.rowcount != 1:
raise Conflict("stale version")
db.execute("INSERT INTO audit VALUES (?,?,?,?,?)",
(issue_id, repo_id, actor, expected_version,
expected_version + 1))
db.commit()
return expected_version + 1
except Exception:
db.rollback()
raise
BEGIN IMMEDIATE acquires the write transaction before reading membership and version. In this small SQLite store, another writer cannot change membership or the issue between that read and the update. The conditional update remains explicit because the invariant belongs in the write itself. This deliberately serializes writers and is not a scalable lock strategy for a large service.
The query selects only the version for an authorized write target. An unavailable target produces the same NotFound exception for a wrong repository, missing issue, absent membership or read-only role. A future HTTP adapter could map that to its chosen not-found response and Conflict to a conflict response; no HTTP status handling is implemented here.
Inputs use bound parameters. Titles are stripped, required to be nonempty and limited to 80 characters under this exercise's contract. The store does not render HTML, so safe output rendering belongs to the presentation layer. Database locking or other storage errors are rolled back and propagated instead of being mislabeled as success.
Pair on atomic audit behavior
The extension is: every successful rename must record actor, repository, issue and old/new versions; an audit-write failure must leave the issue unchanged. The schema deliberately stores no title content in the audit row. It is a small change with a clear failure invariant, rather than a request to build an event platform.
First add the audit insert before commit. Then make the audit table unavailable in a disposable fixture and run an otherwise valid rename. SQLite raises an error; the exception path rolls back the title and version update. With the table present, a success produces exactly one audit row. A permission rejection or conflict produces none.
The table is transactional local history, not an append-only tamper-proof compliance log. A database owner can alter it, and no audit retention, external sink or crash-recovery durability test is provided. If the requirement becomes reliable external delivery, propose a transactional outbox and retry contract as another scoped change rather than pretending this insert solves it.
Tests and a review-ready handoff
Run the broken behavior against the fixture first, then run the same contract checks against the repair. Include authorized success, read-only denial, nonmember denial, wrong-repository denial, missing issue, stale version, current-version success and invalid inputs. Each rejection should check the stored title, version and audit count, rather than merely observing an exception.
Local verification passed 140 checks, including 20 two-connection races and the audit-failure rollback. Save the shown SQL as schema.sql and the corrected Python as fixed.py, then check a stale edit:
import sqlite3
from fixed import rename_issue, Conflict
db = sqlite3.connect(":memory:")
db.executescript(open("schema.sql").read())
db.commit()
assert rename_issue(db, "alice", 1, 10, 1, "First") == 2
try:
rename_issue(db, "alice", 1, 10, 1, "Stale")
except Conflict:
pass
else:
raise AssertionError("stale edit accepted")
assert db.execute("SELECT title,version FROM issues WHERE id=10").fetchone() == ("First", 2)
Use separate SQLite connections for a two-writer race. Both submit expected version 1. Under the repaired transaction, one succeeds and the other gets a conflict after it can read version 2. Assert one final update and one audit row. This checks the local database boundary; it is not a load test or proof of distributed deployment behavior.
The pairing handoff should identify the failing behavior, smallest fix, verification and remaining limits. A useful sample is: “The original patch allowed wrong-repository/read-only changes and stale overwrites. The repair scopes writes to current membership and expected version, and includes atomic audit insertion. Regression checks reproduce the original failures and verify rejection without mutation. Authentication, HTTP mapping and external audit delivery remain outside this store.”
If a reviewer asks whether a stale request should automatically retry, explain why that can erase the conflict: rebasing onto the newest version and submitting the old intent without user confirmation changes the contract. Return the conflict and let the client fetch and reconcile. A request whose response was lost also needs a distinct idempotency design if retries must return an earlier receipt.
Prepare for take-home and live pairing with the same repository
For a take-home simulation, include setup, a small diff, reproduction, tests and a short README. Respect the allotted time and scope; adding unrelated features can make the work harder to review. Reserve the final interval for inspecting the diff, running the documented commands and removing accidental output or unused files.
For live pairing, state the contract and a short plan, then work in visible increments. After reproducing the permission failure, pause to agree on error behavior before editing the transaction. When the audit requirement arrives, explain the added atomicity invariant and its failure test. Share context at decision points instead of narrating every keystroke.
If setup fails, tell the interviewer which operation failed and what you have verified. Avoid spending the entire session silently repairing the environment. A standardized environment can help, as GitHub's team-specific Codespaces article illustrates, but your invitation determines the actual tool.
System design: expand one boundary at a time
For a senior or platform-oriented role, turn this local store into a design discussion. Who owns repository permissions? How do permission changes reach the write path? How are hot repositories isolated? What happens when a client times out after commit? Which audit records can be visible to whom?
Start with the authoritative write path before adding caches. A stale permission cache can authorize work that should be rejected. A read replica can show an older version; optimistic concurrency must still compare against the write authority. For queued side effects, distinguish committed state from delivered notifications and explain recovery after a worker failure.
Do not claim a dedicated system-design session for every GitHub Software Engineer. Confirm the team's depth, then choose a scenario that matches its responsibilities. The value of this mock is that the design discussion has a tested local invariant to extend.
A seven-day plan with concrete outputs
| Day | Work | Artifact |
|---|---|---|
| 1 | Read the posting and confirm assessment format | Role evidence and invitation checklist |
| 2 | Reproduce the proposed PR's two behavior bugs | Failing contract checks and minimal traces |
| 3 | Write prioritized review comments | Two blockers, one suggestion and a summary |
| 4 | Implement the repair and pair on audit insertion | Passing regressions and rollback evidence |
| 5 | Expand permissions, retries and external delivery | One design diagram and rejected alternative |
| 6 | Run a mixed mock and a true project deep dive | Notes on missing context and ownership gaps |
| 7 | Repeat the weakest task and check setup | Corrected work and verified run instructions |
Frequently asked questions
Does GitHub always use a take-home assessment?
No. Its current careers page describes a role-dependent coding or writing assessment. The detailed repository-and-PR workflow in the 2022 article is historical evidence, not a universal 2026 promise.
Should I assume a separate code-review or pairing interview?
Confirm it with the recruiter. GitHub has published a team-specific Codespaces interview example, while its current company-wide page does not guarantee every technical format for every role.
What should I prioritize in a code review?
State the intended behavior, reproduce correctness or authorization failures, and explain their impact and required tests. Separate merge blockers from optional style suggestions.
Does the corrected exercise implement a GitHub endpoint?
No. It is an original Python/SQLite simulation with trusted actor context, repository write permissions, expected-version updates and an atomic audit insert. It runs no GitHub API or HTTP server.
Can I use AI tools in an assessment?
Follow the current invitation and ask if the policy is unclear. A historical article allowing documentation and preferred tools does not establish permission to use a coding assistant in your assessment.
Sources and scope
- GitHub Careers: Experienced Professionals: current role-dependent interview guidance, checked in the browser.
- GitHub: How take-home technical interviews work: historical 2022 repository and review process.
- GitHub: Technical interviews via Codespaces: a team-specific environment and pairing example.
- GitHub: Sarah Vessels on effective code review: specific comments and blockers versus preferences.
- SQLite transactions: local transaction behavior used by the repair.
- OWASP Authorization Cheat Sheet: permission validation and authorization boundaries.
Checked October 8, 2026. The issue store, identities, defects and repair are fictional practice material. Local checks exercise Python and SQLite only; they do not access GitHub, authenticate users or implement a production API. Your current invitation controls the assessment format and allowed tools.