Tech Lead NotesRohit’s interview study guide
Chapter 08

Leading Distributed Teams

Leading engineers in India from Austin: the real overlap windows across US clock changes, handoffs, async specs and tickets, onboarding and mentoring, delegating without micromanaging, feedback across cultures, and one quality bar for every site.

6 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#Working across US and India time zones

Added

India is 10 hours 30 minutes ahead of Austin for most of the year and 11 hours 30 minutes ahead in winter, and that one hour decides whether your teams share 90 minutes a day or 30. India Standard Time is UTC+5:30 all year; India has no daylight saving time. Austin is on US Central time: CDT (UTC−5) from the second Sunday in March to the first Sunday in November, CST (UTC−6) the rest of the year. In 2026 the US changes are on 8 March and 1 November; in 2027, 14 March and 7 November. India never moves, so every US change shifts the whole overlap by an hour.

The mental model is a relay race's exchange zone. Runners can only hand the baton over inside a short marked zone; anywhere else, the baton has to be left somewhere the next runner will find it. The overlap is the exchange zone: the only time a question can be asked and answered in the same conversation. Everything else has to be handed over in writing, clearly labelled, so the next runner can pick it up without asking. A distributed team succeeds or fails on two things: how it uses the zone (only for what needs a conversation), and how good the handovers are outside it.

Austin clock (America/Chicago) 00 03 06 09 12 15 18 21 24 Summer: CDT India 10:30 ahead Austin 08:00–17:00 India 11:00–20:00 IST 90 min: 08:00–09:30 Winter: CST India 11:30 ahead Austin 08:00–17:00 India 11:00–20:00 IST 30 min: 08:00–08:30 India's day starts 23:30 the evening before On a typical 09:30–18:30 IST office day there is no overlap at all in either season: 18:30 IST is 08:00 Austin in summer and 07:00 in winter.
The same two working days, before and after US clocks go back. India doesn't change its clocks, so the India block slides one hour later on the Austin clock and the shared window shrinks from 90 minutes to 30.

Rather than trusting a table, compute it from the time-zone database, which is what calendars do. This model uses Intl.DateTimeFormat with the IANA zones America/Chicago and Asia/Kolkata (Node 26 here ships tz data 2026a), on dates either side of both 2026 US changes, for two India schedules:

overlap.mjs
// Overlap between an Austin team and an India team, from the IANA time-zone rules Node ships with:
// America/Chicago follows US daylight saving time; Asia/Kolkata has no DST (always UTC+5:30).
const AUSTIN = "America/Chicago";
const INDIA = "Asia/Kolkata";

// A zone's offset from UTC in minutes at one instant (e.g. -300 for UTC-5).
function offsetMinutes(zone, instant) {
  const name = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "longOffset" })
    .formatToParts(instant).find((p) => p.type === "timeZoneName").value; // "GMT-05:00"
  const m = name.match(/GMT([+-])(\d\d):(\d\d)/);
  return m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) : 0;
}

// "2026-03-09", "08:00" on the wall clock in `zone` → the real instant.
function wallToInstant(date, time, zone) {
  const asIfUtc = Date.parse(`${date}T${time}:00Z`);
  let guess = asIfUtc - offsetMinutes(zone, new Date(asIfUtc)) * 60_000;
  guess = asIfUtc - offsetMinutes(zone, new Date(guess)) * 60_000; // second pass settles DST edges
  return new Date(guess);
}

const clock = (instant, zone) =>
  new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(instant);
const hm = (min) => `${Math.floor(Math.abs(min) / 60)}:${String(Math.abs(min) % 60).padStart(2, "0")}`;
const utc = (min) => `UTC${min < 0 ? "-" : "+"}${hm(min)}`;

const austinDay = { start: "08:00", end: "17:00" };
const indiaDays = {
  "India 09:30–18:30": { start: "09:30", end: "18:30" }, // a typical office day
  "India 11:00–20:00": { start: "11:00", end: "20:00" }, // a shifted day, common on US-facing teams
};
const dates = ["2026-03-06", "2026-03-09", "2026-10-30", "2026-11-02"]; // either side of both US clock changes

for (const [label, indiaDay] of Object.entries(indiaDays)) {
  console.log(`Austin 08:00–17:00 with ${label}`);
  for (const date of dates) {
    const aStart = wallToInstant(date, austinDay.start, AUSTIN), aEnd = wallToInstant(date, austinDay.end, AUSTIN);
    const iStart = wallToInstant(date, indiaDay.start, INDIA), iEnd = wallToInstant(date, indiaDay.end, INDIA);
    const from = new Date(Math.max(aStart, iStart)), to = new Date(Math.min(aEnd, iEnd));
    const minutes = Math.max(0, (to - from) / 60_000);
    const aOff = offsetMinutes(AUSTIN, aStart), iOff = offsetMinutes(INDIA, aStart);
    const window = minutes
      ? `overlap ${clock(from, AUSTIN)}–${clock(to, AUSTIN)} Austin = ${clock(from, INDIA)}–${clock(to, INDIA)} IST (${minutes} min)`
      : "no overlap";
    console.log(`  ${date}  Austin ${utc(aOff)}  India ${utc(iOff)}  India ${hm(iOff - aOff)} ahead  ${window}`);
  }
}
Result
Austin 08:00–17:00 with India 09:30–18:30
  2026-03-06  Austin UTC-6:00  India UTC+5:30  India 11:30 ahead  no overlap
  2026-03-09  Austin UTC-5:00  India UTC+5:30  India 10:30 ahead  no overlap
  2026-10-30  Austin UTC-5:00  India UTC+5:30  India 10:30 ahead  no overlap
  2026-11-02  Austin UTC-6:00  India UTC+5:30  India 11:30 ahead  no overlap
Austin 08:00–17:00 with India 11:00–20:00
  2026-03-06  Austin UTC-6:00  India UTC+5:30  India 11:30 ahead  overlap 08:00–08:30 Austin = 19:30–20:00 IST (30 min)
  2026-03-09  Austin UTC-5:00  India UTC+5:30  India 10:30 ahead  overlap 08:00–09:30 Austin = 18:30–20:00 IST (90 min)
  2026-10-30  Austin UTC-5:00  India UTC+5:30  India 10:30 ahead  overlap 08:00–09:30 Austin = 18:30–20:00 IST (90 min)
  2026-11-02  Austin UTC-6:00  India UTC+5:30  India 11:30 ahead  overlap 08:00–08:30 Austin = 19:30–20:00 IST (30 min)
What’s happening
  1. offsetMinutes asks Intl for the zone's offset at a specific instant (longOffset gives a name like GMT-05:00) and turns it into minutes. Asking at an instant, not per zone, is the whole point: Chicago is −360 minutes on 6 March and −300 on 9 March.
  2. wallToInstant turns a wall-clock time in a zone ("08:00 in Austin on 9 March") into a real instant. It guesses with the offset at the naive time, then corrects once with the offset at the guess, which settles the days when the offset changes.
  3. With a typical India office day (09:30–18:30 IST) there's no overlap in any season. 18:30 IST is 08:00 Austin in summer and 07:00 in winter, so the India day ends before or as Austin's begins. Teams that "work US hours" in India, or Austin people who start at 07:00, exist because of this row.
  4. With a shifted India day (11:00–20:00 IST, common on US-facing teams), the overlap is 08:00–09:30 Austin = 18:30–20:00 IST in summer, 90 minutes. After 1 November it's 08:00–08:30 Austin = 19:30–20:00 IST, 30 minutes. India's hours didn't change and Austin's didn't change; only the US clocks did.
  5. The difference column reads 11:30 on 6 March, 10:30 on 9 March and 30 October, and 11:30 again on 2 November: India is 11:30 ahead until the US change on 8 March, 10:30 ahead until the change on 1 November, then 11:30 again. Plan the year with both numbers: about eight months at 10:30 and four at 11:30.
  6. If you changed austinDay.start to "07:30", winter overlap doubles to 60 minutes. Small, deliberate shifts on both sides are how teams buy overlap; the model makes the cost of each choice visible.
AdvancedWho absorbs the clock change

A recurring meeting is stored in the time zone of whoever created it. That decides which site's clock moves when the US changes its clocks:

meeting-drift.mjs
// A weekly sync is created in one time zone. Which side's clock moves when the US changes its clocks?
const AUSTIN = "America/Chicago";
const INDIA = "Asia/Kolkata";

function offsetMinutes(zone, instant) {
  const name = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "longOffset" })
    .formatToParts(instant).find((p) => p.type === "timeZoneName").value;
  const m = name.match(/GMT([+-])(\d\d):(\d\d)/);
  return m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) : 0;
}
function wallToInstant(date, time, zone) {
  const asIfUtc = Date.parse(`${date}T${time}:00Z`);
  let guess = asIfUtc - offsetMinutes(zone, new Date(asIfUtc)) * 60_000;
  guess = asIfUtc - offsetMinutes(zone, new Date(guess)) * 60_000;
  return new Date(guess);
}
const show = (instant, zone) =>
  new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(instant);

// 1. The days US clocks change, found by scanning noon UTC each day for a change in Chicago's offset.
for (const year of [2026, 2027]) {
  const changes = [];
  let prev = offsetMinutes(AUSTIN, new Date(Date.UTC(year, 0, 1, 12)));
  for (let d = 1; d <= 365; d++) {
    const noon = new Date(Date.UTC(year, 0, 1 + d, 12));
    const now = offsetMinutes(AUSTIN, noon);
    if (now !== prev) changes.push(`${noon.toISOString().slice(0, 10)} (UTC${now / 60})`);
    prev = now;
  }
  console.log(`${year}: Chicago changes on ${changes.join(" and ")}`);
}

// 2. The same Tuesday sync, owned by each side, on the Tuesdays either side of 1 November 2026.
const tuesdays = ["2026-10-27", "2026-11-03"];
const owners = [
  { name: "Created in Austin, 08:30 America/Chicago", zone: AUSTIN, time: "08:30" },
  { name: "Created in India, 19:00 Asia/Kolkata", zone: INDIA, time: "19:00" },
];
for (const owner of owners) {
  console.log(owner.name);
  for (const date of tuesdays) {
    const at = wallToInstant(date, owner.time, owner.zone);
    console.log(`  ${date}: Austin ${show(at, AUSTIN)}  India ${show(at, INDIA)}`);
  }
}
Result
2026: Chicago changes on 2026-03-08 (UTC-5) and 2026-11-01 (UTC-6)
2027: Chicago changes on 2027-03-14 (UTC-5) and 2027-11-07 (UTC-6)
Created in Austin, 08:30 America/Chicago
  2026-10-27: Austin Tue 08:30  India Tue 19:00
  2026-11-03: Austin Tue 08:30  India Tue 20:00
Created in India, 19:00 Asia/Kolkata
  2026-10-27: Austin Tue 08:30  India Tue 19:00
  2026-11-03: Austin Tue 07:30  India Tue 19:00
What’s happening
  1. The first part finds the US change dates the same way a calendar would, from the tz data: it checks Chicago's offset at noon UTC each day of the year and prints the days it changed. 2026: 8 March (to UTC−5) and 1 November (to UTC−6). 2027: 14 March and 7 November.
  2. A sync created in Austin at 08:30 America/Chicago stays at 08:30 for Austin all year. For India it's 19:00 on 27 October and 20:00 on 3 November: the India team absorbs the change, and their meeting now starts just as their working day ends.
  3. The same sync created in India at 19:00 Asia/Kolkata stays at 19:00 in India. For Austin it moves from 08:30 to 07:30: now the Austin people absorb it.
  4. Neither is wrong, but someone always pays, and it's decided by whoever clicked "create". Decide deliberately, write it down in the team's working agreement, and look at the calendar in the week each US change happens (and the weeks between the US and European changes, if the client has people in Europe too).
  5. The rules themselves can change. The US House passed the Sunshine Protection Act (H.R. 139), which would make daylight time permanent, on 14 July 2026 by 308–117 (House Clerk, roll call 238). It still needs the Senate and the President; news reports up to October 2026 say the Senate hadn't voted on it (checked 27 October 2026 in the news only; the Senate's own record wasn't checked). If it ever becomes law, the tz database is updated and code like this follows it; a table of hard-coded offsets wouldn't. That's the reason to compute from Intl and never to store "India is 10.5 hours ahead".
AdvancedWhat one blocking question costs

The overlap matters most for blocking questions: an India developer can't continue until someone in Austin answers. This model follows one question from the moment it's asked to the moment the developer can act on the answer, with Austin on 08:00–17:00 CT and India on 11:00–20:00 IST, both Monday to Friday:

blocked-question.mjs
// An India engineer gets stuck and asks the Austin lead a question. When can work continue?
// Austin works 08:00–17:00 America/Chicago, India 11:00–20:00 Asia/Kolkata, both Monday–Friday.
const hours = {
  "America/Chicago": { start: 8 * 60, end: 17 * 60 },
  "Asia/Kolkata": { start: 11 * 60, end: 20 * 60 },
};
const wall = (zone, instant) => {
  const p = Object.fromEntries(
    new Intl.DateTimeFormat("en-US", { timeZone: zone, weekday: "short", hour: "numeric", minute: "numeric", hourCycle: "h23" })
      .formatToParts(instant).map((x) => [x.type, x.value]),
  );
  return { day: p.weekday, minute: Number(p.hour) * 60 + Number(p.minute) };
};
const working = (zone, instant) => {
  const { day, minute } = wall(zone, instant);
  return day !== "Sat" && day !== "Sun" && minute >= hours[zone].start && minute < hours[zone].end;
};
// Step forward a minute at a time (fine for a model) until `zone` is at work.
const nextWorking = (zone, instant) => {
  let t = instant.getTime();
  while (!working(zone, new Date(t))) t += 60_000;
  return new Date(t);
};
const fmt = (instant, zone) =>
  new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(instant);
const hoursBetween = (a, b) => ((b - a) / 3_600_000).toFixed(1);

const ASK = "Asia/Kolkata", ANSWER = "America/Chicago";
const questions = [
  "2026-10-27T12:00:00+05:30", // Tuesday lunchtime in India
  "2026-10-27T19:15:00+05:30", // Tuesday evening in India, inside the overlap
  "2026-10-30T19:45:00+05:30", // Friday evening in India
  "2026-11-03T12:00:00+05:30", // Tuesday again, after US clocks went back on 1 November
];
for (const iso of questions) {
  const asked = new Date(iso);
  const seen = nextWorking(ANSWER, asked); // the lead reads it when their day starts
  const answered = new Date(seen.getTime() + 20 * 60_000); // 20 minutes to read, check and reply
  const resumed = nextWorking(ASK, answered); // the engineer can act on it in their working hours
  console.log(
    `asked ${fmt(asked, ASK)} IST → lead sees ${fmt(seen, ANSWER)} Austin → unblocked ${fmt(resumed, ASK)} IST` +
      `  (${hoursBetween(asked, resumed)} h)`,
  );
}
Result
asked Tue 12:00 IST → lead sees Tue 08:00 Austin → unblocked Tue 18:50 IST  (6.8 h)
asked Tue 19:15 IST → lead sees Tue 08:45 Austin → unblocked Tue 19:35 IST  (0.3 h)
asked Fri 19:45 IST → lead sees Fri 09:15 Austin → unblocked Mon 11:00 IST  (63.3 h)
asked Tue 12:00 IST → lead sees Tue 08:00 Austin → unblocked Tue 19:50 IST  (7.8 h)
What’s happening
  1. nextWorking steps forward a minute at a time until the given zone is inside its working hours on a weekday (fine for a model; a real scheduler would jump). The lead reads the question at the start of their day and takes 20 minutes to reply.
  2. A question asked at 12:00 IST on Tuesday is seen at 08:00 Austin (18:30 IST) and answered by 18:50 IST: 6.8 hours, most of the India working day lost. Asked inside the overlap (19:15 IST), the same question costs 20 minutes.
  3. The Friday case is the expensive one. Asked at 19:45 IST on Friday, the lead sees it at 09:15 Austin and answers at 09:35, which is 20:05 IST, five minutes after the India day ended. The developer acts on it at 11:00 IST on Monday: 63.3 hours. Austin's Friday afternoon is India's weekend.
  4. The last row is the same Tuesday question after the US clocks went back: 7.8 hours instead of 6.8, because the Austin day now starts an hour later on the India clock.
  5. What a lead does about it: make most questions non-blocking (good tickets, written decisions, a documented default for each open question), give every India developer a next item to switch to when blocked, answer India questions first thing in the Austin morning before anything else, and never leave a decision for "Friday afternoon".
AdvancedA handoff note that survives the night

When work crosses from one site to the other, the note is the baton. A template that works for an end-of-day handoff (India evening to Austin morning, or the reverse):

End-of-day handoff (posted in the team channel and linked from the ticket)
Handoff: API-298, device settings PATCH, from Dev 3 (India) to Austin, Tue 27 Oct 19:40 IST

State:       Branch feat/api-298 pushed; PR #412 open as draft; CI green except the contract test below.
Done today:  PATCH handler and validation; unit tests for 0, 100, 101, -1.
Not done:    Contract test fails against firmware 9.8: it returns 500, not 400, for level=101.
             Log and request/response saved in the PR description.
Decision needed (blocking): Do we map firmware 9.8's 500 to a 400 in our API, or document 9.8 as
             unsupported for this endpoint? Default if no answer by 11:00 IST: map it, behind a flag.
Next for Austin (optional, if you have time): try the PR against the 9.8 simulator image.
Next for me tomorrow: finish error mapping per the decision; then UI-207.
What’s happening
  1. The header says what, who, from where and when, with the time zone. A reader in Austin at 08:00 knows immediately whether this is theirs and how fresh it is.
  2. State points to artefacts, not descriptions: the branch, the draft PR, the CI status. Anyone can pick it up from the repository without a call.
  3. Not done includes the evidence (log and request/response saved in the PR), so the Austin reader doesn't have to reproduce the problem before thinking about it.
  4. Decision needed is the most important line: one question, marked blocking, with a default and a deadline. If Austin doesn't answer by 11:00 IST, Dev 3 proceeds with the default instead of waiting a day. That turns a blocking question into a non-blocking one.
  5. Next for both sides makes the plan explicit, so the night isn't wasted and nobody duplicates work. The note is linked from the ticket, so it's still findable next month.
AdvancedA team working agreement for two sites

Most friction on distributed teams comes from expectations nobody wrote down: when people are reachable, how fast a reply is due, who takes the late meeting. A working agreement makes them explicit. The team writes it together, in the repository, and reviews it at a retro each time the US clocks change:

docs/working-agreement.md
# Working agreement (Austin + India). Agreed 2026-11-02; review at the March 2027 retro.

Hours
- Austin 08:00–17:00 CT. India 11:00–20:00 IST.
- Overlap: 08:00–09:30 CT / 18:30–20:00 IST (US daylight time); 08:00–08:30 CT / 19:30–20:00 IST (US standard time).
- The overlap is for: Daily Scrum, blocking questions, decisions, live reviews. No status meetings in it.

Replies
- Ticket and PR comments: within the other site's next working day.
- Blocking questions: in the team channel, tagged, with a default. Answered in the next overlap.
- Nobody is expected to read chat outside their working hours. Urgent production issues go to on-call.

Meetings
- Every invite shows both times: "08:00 CT / 19:30 IST".
- Long events (Planning, Review, Retro) rotate between an early Austin slot and a late India slot each Sprint.
- Every meeting has notes in writing within the day; decisions go in docs/decisions.md.

Handoffs
- End-of-day handoff note on any ticket that crosses sites (template: docs/templates/handoff.md).
- Nothing India-bound is left for Friday afternoon Austin time.

Holidays
- Both sites' holiday lists are in the team calendar by 15 January.
What’s happening
  1. Hours state both sites' days and both seasons' overlap windows, with the times computed (as in the model above), so nobody has to work them out or gets them wrong in March and November.
  2. Saying what the overlap is for protects it. Thirty minutes in winter is gone if a status meeting takes it; listing the Daily Scrum, blocking questions, decisions and live reviews, and banning status meetings, keeps it for what only a conversation can do.
  3. Replies set response-time expectations per channel, and say explicitly that nobody reads chat outside their hours. That one sentence stops the slow creep of India engineers answering Austin messages at midnight, and gives production issues a proper path (on-call) instead.
  4. Meetings make the cost of the inconvenient slot shared (rotation) and visible (both times in every invite), and require written notes so the people who couldn't attend aren't second-class.
  5. Review it when the clocks change. The agreement's numbers are only right for half the year each; putting the review in the March and November retros keeps them true.
AdvancedFollow-the-sun: why it's harder than it sounds

Follow-the-sun means passing the same piece of work between sites at the end of each day so it progresses around the clock. It sounds like doubling speed; in software it usually isn't, and the research says why:

  • Herbsleb and Mockus, "An Empirical Study of Speed and Communication in Globally Distributed Software Development" (IEEE Transactions on Software Engineering, 2003), combined change-management data with surveys in a multi-site development organisation (and replicated the analysis in a second one) and found that distributed work items appeared to take about two and a half times as long as similar items done at one site. Their abstract ties the delay to distributed items involving more people, and the number of people to the calendar time an item takes. The practical conclusion usually drawn from it (this page's reading; the paper's full recommendations weren't checked) is to reduce the need for cross-site communication, for example by dividing work so each site can work on its own part independently.
  • Every handoff costs context: the receiver has to rebuild what the sender knew. In development, that rebuild often costs more than the hours gained.
  • Where follow-the-sun does work is in queue-shaped work with small, self-contained units: support tickets, incident response with a good runbook, and monitoring.

The practical shape for a US–India product team is the opposite of follow-the-sun: give each site whole pieces it can finish, a feature, a component or an API, end to end, and hand over at boundaries (contracts, PRs), not mid-task. Use the overlap for decisions and reviews, not for passing half-done work.

AdvancedCalendars: holidays and weekends on both sides
  • Get both holiday lists in January. India's three national holidays have fixed dates (Republic Day 26 January, Independence Day 15 August, Gandhi Jayanti 2 October), but many others (Diwali, Holi, Eid, and regional festivals that differ by state and city) follow lunar calendars and move every year. US holidays mostly fall on fixed weekdays (Thanksgiving is the fourth Thursday of November; Memorial Day the last Monday of May).
  • The week shapes the calendar too. There are five overlap windows a week, but they're lopsided. India's Monday morning is Sunday night in Austin, so India's Monday questions wait for the Monday overlap; Austin's Friday afternoon is India's Friday night, so anything Austin produces after Friday's overlap isn't seen in India until Monday at 11:00 IST, two to two and a half days later. Ask for decisions early in the week, and don't leave India-bound handoffs for Friday afternoon.
  • Put both sites' holidays in the capacity plan (see Estimation and planning), and in release plans: don't schedule a release that needs India support on Diwali, or one that needs Austin sign-off on the Friday after Thanksgiving.
AdvancedTrap: "IST", "CST" and other ambiguous times
What time is it in India?

An Austin product manager books a demo for 09:00 Austin time on Friday 6 March 2026 and again on Friday 13 March 2026. What time is each in India, and is either inside an 11:00–20:00 IST working day?

Show answer

6 March: 20:30 IST, outside the India day. 13 March: 19:30 IST, inside it, with 30 minutes left.

What’s happening
  1. On 6 March 2026 the US is still on standard time (the change is on Sunday 8 March), so Austin is CST, UTC−6. 09:00 CST = 15:00 UTC = 20:30 IST (UTC+5:30), half an hour past the end of the India day.
  2. On 13 March Austin is on CDT, UTC−5. 09:00 CDT = 14:00 UTC = 19:30 IST, inside the India day.
  3. Same Austin time, a week apart, an hour's difference in India, because only one side changed its clocks.
  4. The fix for the 6 March demo is to move it to 08:00 Austin (19:30 IST), or to record it for the India team and take their questions in writing.

#Async communication that works

Added

On a team split by eleven hours, writing is the main interface between people. For most of each day nobody on the other side is available to ask, so any ticket, spec or message that needs a follow-up question costs half a day or more (as the blocking-question model above shows). Async-first teams treat written communication the way engineers treat an API: it has to be complete, unambiguous and stable enough that the consumer can act on it without calling the author. GitLab, an all-remote company that publishes its handbook, describes asynchronous communication as moving work forward without needing the other people to be available at the same time, and makes "handbook first" (write it down where everyone can find it) a core practice.

The mental model is an API contract between people. A good API documents its inputs, outputs, errors and edge cases so a client in another company can integrate without a meeting. A good ticket does the same for a developer in another time zone: context, what "done" means, what's out of scope, how to test it, and what to do if something's unclear. A message like "fix the volume thing as discussed" is an undocumented endpoint: it only works for the one person who was in the room.

Research on remote work points the same way. A study of 61,182 Microsoft employees in the US during the 2020 shift to remote work (Yang et al., "The effects of remote work on collaboration among information workers", Nature Human Behaviour, 2021) found that collaboration became more siloed, with fewer bridges between groups, and that communication became more asynchronous. Distributed teams get the async part whether they plan for it or not; the lead's job is to make it good, and to deliberately build the cross-site bridges that otherwise wither.

Here's the core artefact, a ticket that can be picked up in India at 11:00 IST with nobody awake in Austin. The endpoint is illustrative, not a real Poly or HP API:

Ticket template (illustrative example filled in)
# PUT /api/v1/audio/volume rejects out-of-range values with 400

## Context
The web UI slider can send 101 when dragged fast, and the API stores it.
Decision to validate on the API side: ADR-014 (link). Slider fix is separate: UI-212.

## Acceptance criteria
- [ ] {"level": 101} or {"level": -1} returns 400 with the problem+json body from the API guide
- [ ] {"level": 0} and {"level": 100} still return 204
- [ ] After a rejected request, GET returns the previous level unchanged

## Out of scope
Changing the slider (UI-212). Other audio settings.

## How to test
npm run test:contract -- volume, then curl the device simulator (README, "Local API").

## Questions
Tech lead (Austin) answers in the 08:00–09:30 CT overlap.
If blocked outside it: write the question here, take UI-198 next, and assume
"reject with 400" for any value outside 0–100 unless told otherwise.
What’s happening
  1. The title states the outcome in terms someone can verify ("rejects out-of-range values with 400"), not the activity ("Fix volume API"). Someone scanning the board in another time zone knows what it is without opening it.
  2. Context explains why, and links the decision (an ADR) and the related ticket, so the developer doesn't have to reconstruct a conversation they weren't in.
  3. Acceptance criteria are a checklist of observable results, including the edge values 0 and 100 that must still work and the "nothing changed" check after a rejection. The developer builds to them, the reviewer checks them, and "done" isn't a matter of opinion.
  4. Out of scope prevents the most common async failure: a conscientious developer "also fixing" the slider, colliding with UI-212.
  5. How to test gives the exact commands, so verifying doesn't require tribal knowledge.
  6. Questions says who answers, when they're reachable (in the overlap, with the time zone), what to do if blocked outside it (another ticket to switch to), and a default assumption. That last line converts a blocking question into a non-blocking one, which saves a day each time it's used.
AdvancedChecking tickets before they cross the ocean

A check like this can run in refinement (or as a bot on the issue tracker). It looks for the sections the template requires and for words that only make sense to someone who was in the room:

ticket-lint.mjs
// A ticket has to survive a night with nobody to ask. This checks one for the parts that make that possible.
const required = ["Context", "Acceptance criteria", "Out of scope", "How to test", "Questions"];
const vague = [/\basap\b/i, /\betc\.?/i, /\bproperly\b/i, /\bas discussed\b/i, /\bsame as before\b/i, /\bshould just work\b/i];

function lint(ticket) {
  const sections = new Map();
  let current = null;
  for (const line of ticket.split("\n")) {
    const h = line.match(/^##\s+(.+)/);
    if (h) sections.set((current = h[1].trim()), []);
    else if (current && line.trim()) sections.get(current).push(line.trim());
  }
  const problems = [];
  for (const name of required) if (!sections.get(name)?.length) problems.push(`missing or empty: ${name}`);
  const criteria = sections.get("Acceptance criteria") ?? [];
  if (criteria.length && !criteria.every((c) => /^- \[ \] /.test(c))) problems.push("acceptance criteria must be a checklist");
  for (const re of vague) {
    const hit = ticket.match(re);
    if (hit) problems.push(`vague: "${hit[0]}"`);
  }
  if (/\bslack\b|\bcall\b/i.test(ticket) && !/https?:\/\//.test(ticket)) problems.push("refers to a conversation but links nothing");
  return problems;
}

const before = `# Fix the volume API
As discussed on the call, the volume endpoint should handle bad values properly, same as before. ASAP please.`;

const after = `# PUT /api/v1/audio/volume rejects out-of-range values with 400
## Context
The web UI slider can send 101 when dragged fast; the API currently stores it. Decision: https://example.test/adr/014
## Acceptance criteria
- [ ] A body of {"level": 101} or {"level": -1} returns 400 with the problem+json body from the API guide
- [ ] {"level": 0} and {"level": 100} still return 204
- [ ] The stored level is unchanged after a rejected request
## Out of scope
Changing the slider itself (separate ticket UI-212).
## How to test
Run the contract tests: npm run test:contract -- volume. Then curl the device simulator (README, "Local API").
## Questions
The tech lead (Austin) answers in the 08:00–09:30 CT overlap; if blocked outside it, take UI-198 next.`;

for (const [name, ticket] of [["before", before], ["after", after]]) {
  const problems = lint(ticket);
  console.log(`${name}: ${problems.length ? problems.length + " problems" : "ready to pick up"}`);
  for (const p of problems) console.log(`  - ${p}`);
}
Result
before: 10 problems
  - missing or empty: Context
  - missing or empty: Acceptance criteria
  - missing or empty: Out of scope
  - missing or empty: How to test
  - missing or empty: Questions
  - vague: "ASAP"
  - vague: "properly"
  - vague: "As discussed"
  - vague: "same as before"
  - refers to a conversation but links nothing
after: ready to pick up
What’s happening
  1. lint splits the ticket into ## sections and checks that each required one exists and isn't empty. The "before" ticket has a title and one sentence, so all five sections are missing.
  2. The acceptance criteria must be a checklist (- [ ] …), because a checklist is what a reviewer ticks off. Prose criteria ("it should handle bad values") pass by opinion.
  3. The vague-word list catches phrases that encode missing context: "ASAP" (by when, exactly?), "properly" (meaning what?), "as discussed" (where? who wasn't there?), "same as before" (before what?). Each one is a question the India developer would have to ask, at a cost of hours.
  4. "As discussed on the call" with no link is flagged separately: if a decision was made in a conversation, the ticket must link to where it was written down.
  5. The "after" ticket, a shortened version of the main example's, passes. A check doesn't make tickets good, but it makes the minimum visible, and it's a neutral way to raise the bar without a lead rewriting people's tickets.
AdvancedA one-page spec for anything bigger than a ticket

Features that span several tickets need a short written spec before refinement. It doesn't need to be a full design document (see Writing a design document for those); one page is usually enough:

docs/specs/firmware-scheduling.md
# Firmware update scheduling (one-page spec)
Status: Draft → Reviewed (Austin + India) → Agreed on 2026-11-04
Owner: Dev 2 (India). Reviewers: tech lead, client PO.

Problem:   Admins update devices one at a time and do it after hours by hand.
Goal:      An admin schedules an update for a group of devices at a chosen local time.
Non-goals: Rollback; per-device maintenance windows; mobile UI.
Users and flow: (3 steps, with a sketch or screenshot link)
API:       POST /v1/schedules {deviceIds, firmwareVersion, runAt (ISO 8601 with offset)} → 202 + Location
           GET /v1/schedules/{id} → status per device
Edge cases: device offline at runAt; device already on that version; time in the past; DST at runAt.
Open questions (with defaults): Q1 device reports progress? (default: poll every 30 s, spike SP-14)
Rollout:   behind a flag; one pilot site first.
Test plan: unit (scheduler), contract (device API), e2e (schedule → simulator updates).
What’s happening
  1. Status and owner make it clear where the spec is in its life and who drives it. The owner here is an India developer: writing specs is a growth step, and it moves design ownership to where the work is done.
  2. Goal and non-goals prevent scope drift. Non-goals are as important as goals: "rollback" being out is a decision, written down, not a gap.
  3. The API sketch is the contract between the UI and API work, so the two can proceed in parallel at both sites. Note runAt with an offset, and "DST at runAt" in the edge cases: a scheduling feature built by a US–India team should know better than anyone that local times move.
  4. Open questions carry defaults, exactly like tickets, so the spec can be agreed and the work started before every answer is in.
  5. The spec is reviewed in writing by both sites over a day or two, then agreed. A short call in the overlap settles only what the comments couldn't.
AdvancedDecision logs and recorded demos

Two habits keep context from staying in Austin:

  • A decision log in the repository: one short entry per decision that someone might later ask "why?" about. Date, decision, why, alternatives considered, who decided. Architecture-level ones become ADRs (see Architecture decision records); smaller ones can be one line each in docs/decisions.md. The rule: if it was decided in a call or a chat, it isn't decided until it's written in the log.
  • Recorded demos and walkthroughs of three to five minutes: a developer shows a finished feature, a tricky bug's cause, or how a part of the system works, and posts the link in the ticket. The other site watches at 1.5× in their own morning. They're also the best onboarding material a team ever produces, because they show the system as it really is.

And norms about channels, written into the team's working agreement:

NeedChannelExpected response
Anything someone will need later (decisions, specs, how-tos)repository docs, ticket, ADRnot a conversation; it's the record
A question about one piece of workcomment on the ticket or PRwithin the other site's next working day
A blocking questionthe team channel, tagged, with a defaultin the next overlap
Production incidentthe on-call page, then a callimmediately, per the on-call rota
Disagreement, feedback, anything sensitivea call (video on), then a written summaryscheduled in the overlap
AdvancedTrap: decisions in direct messages
Rewrite the message

At 16:30 Austin time you need to tell an India developer, who's offline, that the PR they opened today needs changes: the validation should live in the API layer, not in the UI. Your first draft is "Hey, can we chat about your PR tomorrow?". What's wrong with it, and what would you send instead?

Show answer

It wastes the India developer's whole morning: they'll read it at 11:00 IST, with nobody in Austin awake until 18:30 or 19:30 IST, and they can't act on it. Send the content, not a request for a meeting.

What’s happening
  1. "Can we chat?" carries no information and creates worry. The India developer reads it at the start of their day, can't do anything about it until the overlap, and probably stops work on the PR in the meantime.
  2. A better message, posted on the PR: "Thanks for this. One change before merge: please move the range check into the API handler (see ADR-014: the API is the source of truth, the UI may also check but mustn't be the only check). The rest looks good. If you'd rather keep it in the UI for a reason I'm missing, say so in the PR and we'll talk at 08:00 CT / 19:30 IST."
  3. It says what to change and why (with a link), says what's fine, gives a way to disagree, and names when a conversation can happen if needed. The developer can do the work in their morning and have it ready for review when Austin wakes up.
  4. The general rule: never send a message whose only content is "let's talk". If the topic needs a conversation (sensitive feedback, for example), still say what it's about and when.

#Onboarding and mentoring offshore engineers

Added

Onboarding is the highest-leverage thing a lead does for a distributed team, and for an engineer eleven hours away the default outcome is isolation. A new joiner in the Austin office absorbs context by overhearing, asking the person at the next desk and joining hallway conversations. A new joiner in India gets none of that: they get a laptop, a wiki link and a lead who's asleep for most of their day. Unless onboarding is designed, they end up as a ticket-taker who doesn't know why anything is the way it is, which wastes them and slowly lowers the team's quality.

The mental model is scaffolding on a building. At the start there's a lot of it: a named buddy, a planned first week, small first tasks with fast reviews, daily check-ins. As the structure becomes self-supporting, you take scaffolding away on purpose: fewer check-ins, bigger tasks, design ownership. Leaving it up forever is micromanagement; taking it down too early is how people fall. The plan says when each piece comes down.

Here's a checklist for the first 90 days of an India-based engineer joining a team led from Austin. It lives in the repository (or the team wiki) and is copied into a ticket for each new joiner, so both the joiner and the lead can see progress:

docs/onboarding/checklist.md
# Onboarding: <name>, starting <date>. Buddy: <India-based engineer>. Lead: <Austin>.

## Before day 1 (lead)
- [ ] Accounts and access requested: repo, CI, tracker, chat, device simulator / lab devices, VPN
- [ ] A first ticket chosen: small, real, touches UI + API + tests; reviewed in a day
- [ ] Welcome call booked in the overlap (08:00 CT / 19:30 IST in winter); buddy invited

## Week 1: running and shipping something small
- [ ] Local setup works from the README alone (every gap found is fixed in the README by the joiner)
- [ ] Watched the recorded architecture walkthrough and device-API notes; questions posted in the channel
- [ ] First PR merged, with a review from the buddy and one from Austin
- [ ] Knows the working agreement: overlap hours, how to ask a blocking question, default-if-blocked rule

## Days 2–30: one area, end to end
- [ ] Owns 3–5 tickets in one area (e.g. the device list screen and its API)
- [ ] Paired with the lead twice in the overlap and with the buddy daily
- [ ] Has reviewed at least 5 PRs from others (both sites), not only received reviews
- [ ] 30-day check-in (lead + joiner): what's unclear, what's slow, what they want to own next

## Days 31–60: ownership
- [ ] Owns a feature from refinement to release, including the one-page spec
- [ ] Demos their own work in the Sprint Review
- [ ] On the review rota for their area

## Days 61–90: multiplying
- [ ] Leads a design discussion or an ADR in their area
- [ ] Improves one piece of the onboarding for the next joiner
- [ ] 90-day review: growth plan agreed for the next 6 months
What’s happening
  1. Before day 1 is the lead's work. Access requests take days across companies and VPNs, and a joiner who spends week one waiting for access learns that nobody planned for them. The first ticket is chosen in advance: small enough to merge in the first week, real enough to matter, and cutting through every layer so it teaches the system.
  2. Week 1 ends with something merged. "Local setup works from the README alone" is deliberately the joiner's test of the docs: every gap they hit, they fix in the README, which improves onboarding for the next person and gives them a first contribution on day 2.
  3. The working agreement is taught explicitly in week 1: when the overlap is, how to ask a blocking question, and the default-if-blocked rule. In an office this is absorbed; across time zones it has to be said.
  4. Days 2–30 build depth in one area rather than scattering the joiner across the codebase, and include reviewing others' PRs, both sites' PRs. Reviewing is how people learn a codebase fastest, and it makes review a two-way street rather than "Austin approves India".
  5. Days 31–90 move from doing tasks to owning outcomes: a feature with its spec, demoing their own work in the Review (not the lead demoing it for them), joining the review rota, leading a design discussion. The last items, improving onboarding and a growth plan, are where the scaffolding comes down.
  6. The 30-day and 90-day check-ins are scheduled at the start. Without dates, they slip, and the joiner never gets the conversation that tells them how they're doing. For the lead's own 30-60-90 days on a new team, see The first 30, 60 and 90 days on a new team or contract.
AdvancedChecking the first week's calendar

The live sessions in an onboarding plan have to land inside both working days. This model checks a first-week schedule for a joiner starting on 9 November 2026 (US standard time, so the overlap is only 08:00–08:30 Austin with India on 11:00–20:00 IST):

onboarding-week.mjs
// Week 1 for a new engineer in India with a lead in Austin: does every live session land in both working days?
// Austin works 08:00–17:00 America/Chicago; the India team works 11:00–20:00 Asia/Kolkata.
const zones = { austin: "America/Chicago", india: "Asia/Kolkata" };
const day = { austin: [8 * 60, 17 * 60], india: [11 * 60, 20 * 60] };

const parts = (zone, instant) =>
  Object.fromEntries(
    new Intl.DateTimeFormat("en-US", { timeZone: zone, weekday: "short", hour: "numeric", minute: "numeric", hourCycle: "h23" })
      .formatToParts(instant).map((p) => [p.type, p.value]),
  );
const label = (zone, instant) => {
  const p = parts(zone, instant);
  return `${p.weekday} ${p.hour.padStart(2, "0")}:${p.minute}`;
};
const inDay = (who, start, minutes) =>
  [start, new Date(start.getTime() + minutes * 60_000 - 1)].every((t) => {
    const p = parts(zones[who], t);
    const m = Number(p.hour) * 60 + Number(p.minute);
    return !["Sat", "Sun"].includes(p.weekday) && m >= day[who][0] && m < day[who][1];
  });

// Times are written with their own UTC offset, the way a calendar invite stores them. Week of 9 November 2026 (CST).
const sessions = [
  ["Welcome call and goals", "2026-11-09T08:00:00-06:00", 30],
  ["Architecture walkthrough (recorded)", "2026-11-10T08:00:00-06:00", 45],
  ["Pair on first ticket", "2026-11-11T19:30:00+05:30", 30],
  ["Code review of first PR, live", "2026-11-12T14:00:00-06:00", 30],
  ["Friday check-in", "2026-11-13T07:30:00-06:00", 30],
];
for (const [name, iso, minutes] of sessions) {
  const start = new Date(iso);
  const ok = inDay("austin", start, minutes) && inDay("india", start, minutes);
  const where = ok ? "ok" : `outside ${["austin", "india"].filter((w) => !inDay(w, start, minutes)).join(" and ")} hours`;
  console.log(`${name.padEnd(36)} Austin ${label(zones.austin, start)}  India ${label(zones.india, start)}  ${minutes} min  ${where}`);
}
Result
Welcome call and goals               Austin Mon 08:00  India Mon 19:30  30 min  ok
Architecture walkthrough (recorded)  Austin Tue 08:00  India Tue 19:30  45 min  outside india hours
Pair on first ticket                 Austin Wed 08:00  India Wed 19:30  30 min  ok
Code review of first PR, live        Austin Thu 14:00  India Fri 01:30  30 min  outside india hours
Friday check-in                      Austin Fri 07:30  India Fri 19:00  30 min  outside austin hours
What’s happening
  1. Each session is stored with its own UTC offset (-06:00 for Austin in November, +05:30 for India), the way a calendar invite stores an instant. The model prints it on both clocks and checks every minute of it against both working days.
  2. The welcome call at 08:00 Austin is 19:30 IST: inside both days for its 30 minutes. The pairing session, booked by the buddy as 19:30 IST, is the same instant and also fine.
  3. The architecture walkthrough is 45 minutes, so it runs to 20:15 IST, outside the India day. In winter the overlap is only 30 minutes, which is why the plan says "recorded": record it at a time that suits the presenter and give the joiner a 30-minute live Q&A instead.
  4. The live code review at 14:00 Austin is 01:30 on Friday in India. Someone booked it from their own calendar without looking. Live reviews belong in the overlap; otherwise review in writing on the PR.
  5. The Friday check-in at 07:30 Austin is 19:00 IST: fine for India, but 30 minutes before the Austin day. It's a small, reasonable ask of the lead, but the model flags it so it's a choice, not an accident.
AdvancedPairing and mentoring across the time difference
  • Overlap pairing: 30–60 minutes, a clear goal ("get the failing contract test passing"), the joiner driving. Short and frequent beats long and rare.
  • Async pairing: the joiner opens a draft PR early with questions as review comments on their own code; the lead answers in the Austin morning; the joiner continues in their day. It's slower than live pairing but costs no one's evening.
  • A local buddy in India for everything that doesn't need the lead: setup, conventions, "who knows about X". It spreads mentoring load and builds the India team's own leadership.
  • Teach the reasoning, not just the fix. In a review comment, "move this to the API layer" teaches less than "the API is the source of truth because other clients call it too (ADR-014), so validation must live there; the UI can repeat it for fast feedback". The reason travels to the next decision.
  • Growth plans written down: one or two skills to build in the next quarter (for example "own an API design end to end", "lead a refinement session"), with the opportunity that will build each one, and a date to review. Stretch assignments go to India-based engineers as often as to Austin ones; if all design work stays in Austin, so does all the growth.
AdvancedA growth plan on one page

At the 90-day review (and then every six months), write the plan down together. It makes growth concrete and gives the engineer something to hold the lead to:

Growth plan: <name>, agreed <date>, next review <date + 6 months>
Where you are:   Owns the device list area; reviews well; hasn't yet led a design.
Goal 1:          Design and lead a feature end to end, including the client conversation.
  Opportunity:   Firmware scheduling (Q1); you write the spec and present it at the Review.
  Support:       Lead reviews the spec draft with you; you run the design discussion.
  Evidence:      Spec agreed; feature released to the pilot site; you answered the client's questions.
Goal 2:          Mentor the next joiner in India as their buddy.
  Opportunity:   New joiner starting in January.
  Support:       Buddy checklist; a 15-minute weekly check-in with the lead about how it's going.
  Evidence:      Joiner merges a PR in week 1 and owns an area by day 30.
What the lead commits to: one 1:1 every two weeks in the overlap; introductions to the client's
                 firmware team; feedback on each spec within one working day.
What’s happening
  1. Where you are is a short, honest, shared picture. It's written with the engineer, not about them.
  2. Each goal is a capability, not a task ("design and lead a feature end to end"), and each has a real opportunity attached. Growth without an opportunity is a wish; across sites, the lead has to deliberately route interesting work to India-based engineers.
  3. Support says what the lead (or someone else) will do to help, and evidence says how both will know it happened. That makes the next review a conversation about facts.
  4. The last line is the lead's commitments. A growth plan where only one side has obligations isn't a plan; it's an evaluation.
AdvancedTrap: the offshore team only gets bug fixes
The silent new joiner

Three weeks in, your new India-based engineer has merged one PR and asks almost no questions. Their buddy says they seem stuck but won't say so. What do you do?

Show answer

Treat silence as a signal, not as "fine". Make asking cheap and explicit, check for blockers you can't see, and adjust the scaffolding, without making it feel like a performance problem.

What’s happening
  1. Silence in a new remote joiner often means they don't want to look slow, or they don't know asking is expected, especially if they're used to a hierarchy where you don't question the lead. Don't read it as either competence or laziness.
  2. Have a 1:1 in the overlap with specific, easy questions: "what's the slowest part of your day?", "what did you try on the device simulator setup?", "what would you need from me to finish UI-207 this week?". Specific questions get specific answers; "any problems?" gets "no".
  3. Look for invisible blockers: access still missing, a simulator that doesn't run on their machine, a ticket that depends on an unanswered client question. Three weeks with one PR often means one of these.
  4. Add scaffolding for a while: a smaller next ticket, a daily 15-minute pairing slot with the buddy, and a shared rule that a blocker unresolved after an hour goes in the channel with the "default if blocked" plan.
  5. Make asking visible and normal: you ask questions in the public channel yourself, and you thank people publicly for raising blockers. Then review in two weeks whether PRs are flowing.

#Delegation and trust without micromanaging

Added

Delegation means handing over an outcome together with the context and the decision rights to achieve it; micromanaging is handing over a task and keeping the decisions. The lead's time is the team's bottleneck across time zones: if every decision has to wait for the Austin morning, the India developers lose a day each time. Delegating decisions, not just tasks, is how a distributed team moves at the speed of the people doing the work.

The mental model is guardrails on a mountain road. You don't steer someone's car from the passenger seat; you build the guardrails (the constraints, the standards, the definition of done) and let them drive. The more dangerous the road, the stronger the guardrails and the more often you check in, but you still don't grab the wheel. Trust grows the same way: start with low-risk roads, widen the range as the driver shows judgement.

Jurgen Appelo's Management 3.0 gives a vocabulary for "how delegated" a decision is, the seven levels of delegation used in Delegation Poker: tell (I decide and announce), sell (I decide and explain why), consult (I ask for input, then decide), agree (we decide together), advise (you decide, after hearing my advice), inquire (you decide, and tell me afterwards), and delegate (you decide; I don't need to know). The point isn't to delegate everything to level 7; it's to make the level explicit per decision, so nobody guesses. A delegation board writes it down:

docs/working-agreement/delegation.md
# Who decides what (agreed 2026-11-02, review every quarter)

| Decision                                       | Level        | Notes |
|------------------------------------------------|--------------|-------|
| How to implement a ticket (within our standards) | 7 Delegate   | the developer decides |
| Refactoring within a component you own         | 6 Inquire    | mention it in the PR description |
| Choosing a new npm dependency                  | 5 Advise     | ask the lead's view in the PR; you decide |
| Changing a public API's shape                  | 4 Agree      | one-page spec agreed by lead + owner + client contact |
| Splitting or re-sizing items in refinement     | 7 Delegate   | the Developers (Scrum Guide: they size) |
| Accepting a mid-Sprint request                 | 2 Sell       | Product Owner decides; lead explains the impact |
| Production hotfix outside Austin hours         | 6 Inquire    | follow the runbook; tell the lead in the channel |
| Hiring decisions for the team                  | 3 Consult    | the team interviews; the manager decides |
What’s happening
  1. Each row is a decision, not a task. "How to implement a ticket" at level 7 means the India developer never waits for Austin to approve an approach; the guardrails are "within our standards" (linters, review, the Definition of Done).
  2. Levels go down as decisions get harder to reverse. A refactor inside your own component is cheap to undo (6, just say so in the PR). A new dependency has long-term cost (5, ask for advice). A public API's shape is a one-way door for clients (4, agreed in writing). This mirrors the one-way vs two-way door idea in Shifting priorities and incomplete documentation.
  3. Some levels are set by roles, not by trust: accepting a mid-Sprint request belongs to the Product Owner by the Scrum Guide, so for the lead it's at most "sell".
  4. The hotfix outside Austin hours row matters most on a distributed team. Without it, the India developer either waits eight hours with production broken or acts and worries about being blamed. With it, they follow the runbook and inform; the time zone stops being a blocker.
  5. The board has a date and a review cadence. As people show judgement, rows move up a level, which is a concrete, visible form of trust growing.
AdvancedA delegation brief

When handing over something bigger than a ticket (a feature, an investigation, a client-facing task), a short written brief prevents both micromanaging and abdication:

Delegation brief
Outcome:      Admins can schedule firmware updates for a group of devices (pilot site by 2026-12-11).
Why it matters: Client's biggest request from the October Review; unblocks their pilot.
Constraints:  Public API shape agreed with client contact (level 4); within our standards otherwise.
Decision rights: Design, ticket breakdown, and implementation are yours (level 7).
              New dependencies: ask my view (level 5).
Resources:    Dev 4 pairs with you for the first Sprint; simulator images for firmware 9.8 and 10.2.
Check-ins:    Spec review on 2026-11-06; then a 15-minute look at the board every Tuesday in the overlap.
Escalate if:  The pilot date is at risk, or the device can't report progress (spike SP-14 says so).
What’s happening
  1. Outcome and why delegate the goal, not the steps. Knowing why ("unblocks the client's pilot") lets the owner make good trade-offs the lead never anticipated.
  2. Constraints and decision rights use the delegation levels, so the owner knows exactly what's theirs and what needs agreement. Ambiguity here is what makes people ask permission for everything (or nothing).
  3. Resources make the delegation realistic: a pairing partner and the simulator images. Delegating without resources is abdication.
  4. Check-ins are scheduled in advance and are about the work's risk, not the person: a spec review at the start (the riskiest point) and a short weekly look. Scheduled check-ins replace ad hoc "how's it going?" messages, which feel like surveillance across a time zone.
  5. Escalate if names the conditions under which the owner should pull the lead in. That's what lets the lead stay out the rest of the time, comfortably.
AdvancedMicromanaging, abdicating, and the middle
MicromanagingDelegating wellAbdicating
assigns tasks with stepshands over outcomes with contexthands over a vague goal and disappears
approves every approachsets guardrails and decision levelsno standards, no decision levels
frequent unscheduled "status?" messagesscheduled check-ins scaled to riskno check-ins until the deadline
rewrites the work in reviewcoaches in review: explains whyrubber-stamps reviews
takes over when it's hardhelps unblock, keeps ownership with themblames them when it fails

Two classic traps sit at the edges. Reverse delegation, described in "Management Time: Who's Got the Monkey?" (William Oncken Jr. and Donald Wass, Harvard Business Review, 1974): a developer brings a problem, the lead says "let me look into it", and now the lead owns the problem. Reply instead with "what do you recommend?" and help them decide. And trust by visibility, not surveillance: you don't need status pings if the board, the PRs and the handoff notes show the state of the work. Make the work visible and the trust follows.

AdvancedTrap: "just ping me if you have questions"
Which level?

An India-based developer you've worked with for six months wants to replace the hand-written date handling in the scheduling service with a well-known date library. It touches about 15 files, all inside the component they own; the public API doesn't change. Which delegation level, and what do you ask for?

Show answer

Level 5, advise, or 6, inquire: it's their decision, inside their component, reversible, with no API change. A new dependency is the one thing worth a quick view.

What’s happening
  1. It's inside a component they own and the public API is unchanged, so clients aren't affected: a two-way door. That points high on the scale.
  2. It adds a dependency, which has long-term cost (security updates, bundle size, licence), and the board says new dependencies are level 5: they ask for your view in the PR and then decide.
  3. Six months of good judgement is a reason to keep it there rather than pulling it down to "agree". If they'd been on the team two weeks, you might pair on the decision instead.
  4. Ask for evidence in the PR, not permission: why this library (maintenance, size, licence), how it was tested (especially around DST and offsets, the point of the change), and a rollback plan. That's coaching through the guardrails, not approving the approach.

#Cultural awareness and giving feedback across cultures

Added

Culture changes how people signal disagreement, give and hear criticism, and relate to a lead's authority, and on a US–India team those differences are easy to misread in both directions. A US lead may hear silence as agreement and a soft "we'll try" as a commitment; an Indian engineer may hear a US lead's casual "maybe consider…" as optional when it was meant as a requirement, or hear blunt public criticism as a loss of face. None of this is about anyone being right; it's about the message received differing from the message sent.

The most useful framework for this is Erin Meyer's The Culture Map (2014; Meyer is a professor at INSEAD), which places countries on eight scales: communicating (low-context, explicit, vs high-context, implicit), evaluating (direct vs indirect negative feedback), persuading, leading (egalitarian vs hierarchical), deciding, trusting, disagreeing and scheduling. Meyer places the US among the most low-context cultures (say it explicitly, repeat it in writing) and India among the high-context ones (meaning is carried by context, relationship and what isn't said). Two cautions come with the framework: the positions are relative (what matters is the gap between two cultures, not an absolute label), and each position is a midpoint of what's normal in a country, so individuals vary widely. India in particular is enormously diverse in languages and regional cultures, and many Indian engineers have worked with US clients for years. Use the scales to question your own reading of a situation, never to predict a person.

The mental model is two radios tuned slightly differently. Neither radio is broken. If you know the other is tuned a little differently, you speak more clearly, check the message arrived ("can you play back what you'll do?"), and don't assume static means agreement.

Feedback is where this matters most. Here's a piece of feedback rewritten using SBI (Situation–Behaviour–Impact, from the Center for Creative Leadership): describe the situation, the observable behaviour and its impact, then ask, rather than judging the person.

Feedback: before and after (illustrative)
BEFORE (posted in the team channel at 16:00 Austin time):
  "This PR isn't good enough. You need to be more careful with error handling."

AFTER (1:1 call in the overlap, then a short written summary on the PR):
  Situation: "In PR #412 yesterday, the device settings endpoint..."
  Behaviour: "...returns 500 when the device rejects a value, and the tests only cover the success path."
  Impact:    "The web UI shows 'Something went wrong' instead of telling the admin the value is invalid,
              and the client's QA flagged it in the demo."
  Ask:       "What do you think happened here? What would help: the error-mapping section of the
              API guide, or pairing on the first one?"
  Agree:     "Next PR: map device errors to 4xx per the guide, and add a test per error case."
What’s happening
  1. The before version judges the person ("isn't good enough", "be more careful"), gives nothing specific to change, and is public. Public criticism lands harder in more hierarchical, face-conscious contexts, and in any culture it puts the person on the defensive in front of colleagues.
  2. Situation anchors it to one specific event (PR #412, yesterday), so it's about the work, not about the person in general.
  3. Behaviour is observable and checkable: the endpoint returns 500, the tests cover only the success path. Nobody can disagree about what happened, only about why.
  4. Impact connects it to something that matters (the admin's experience, the client's QA flagged it). This is what makes the feedback important without making it harsh, and it works across the direct–indirect spectrum: it's clear about the problem and kind about the person.
  5. Ask invites their view first (there may be a reason: perhaps the API guide was unclear) and offers help. Agree ends with one concrete, checkable change, which then goes in writing on the PR so there's no ambiguity later.
  6. Done 1:1 and live, in the overlap, with video: tone carries more than text, especially for criticism. Positive feedback can be public and written; corrective feedback is private and spoken first, written second.
AdvancedMaking disagreement safe

A lead from a low-context, egalitarian culture often says "push back if you disagree!" and is surprised when nobody does. On a team where the lead is senior, remote and from the client's side, saying "I think you're wrong" can feel like a real risk. Ways to make it easier:

  • Ask for options, not yes/no. "What are two ways we could do this, and what would you pick?" gets real views; "does this plan look OK?" gets "yes".
  • Ask what would make it fail. "If this design goes wrong in six months, what will the reason be?" makes criticism a contribution, not a challenge.
  • Collect views in writing before the meeting (comments on a spec, a shared board). People who wouldn't interrupt a senior person on a call will write a careful comment.
  • Go round in order, juniors first, in design discussions, so nobody is anchored by the lead's opinion.
  • Reward the first person who disagrees, visibly: "That's a good catch, let's change it." The team learns from what happens to them.
  • Ask for a playback, not for questions. "Any questions?" → silence. "Can you summarise what you'll build first and how you'll test it?" → you find out what was understood.
AdvancedWords that mean different things

English is the shared language, but usage differs between US English and Indian English. A few that cause real confusion:

PhraseCommon Indian English meaningWhat a US reader may assume
"I'll revert" / "revert back"I'll replyI'll undo something
"Please do the needful"please do what's requiredvague, unclear request
"Prepone the meeting"move it earlier(not a US word)
"Kindly"polite "please"oddly formal
"Doubt" ("I have a doubt")a questiondistrust or scepticism

And US usages that confuse the other way:

PhraseUS meaningPossible reading elsewhere
"Let's table this"postpone itin British usage, bring it up now
"Ballpark", "touch base", "circle back"rough estimate, talk briefly, return to lateridioms, unclear to non-native speakers
"That's interesting" / "maybe consider…"sometimes a polite "no" or a real requestliteral: optional
"Quite good"very good(British: fairly good)

The rule for a lead: write plainly and literally, avoid idioms and sports metaphors in anything that matters, and if something is a requirement, say "please change", not "maybe consider".

AdvancedTrap: cultural generalisations about individuals
"Yes, it will be done"

On Tuesday you ask an India-based developer whether the device list API will be ready for Thursday's client demo. They say "Yes, it will be done." On Thursday morning it isn't. What might have happened, and what would you do differently next time?

Show answer

"Yes" may have meant "I understand and I'll try my best", not "I'm confident it'll be done", and you didn't create a safe way to say "it's at risk". Next time, ask questions that produce information instead of a yes.

What’s happening
  1. A closed question from a senior person ("will it be ready?") invites "yes" in any culture, more so where contradicting a lead is uncomfortable. The answer told you about the relationship, not the work.
  2. Ask in a way that makes risk normal to mention: "What's left to do on it, and what could stop it being ready by Thursday?" or "On a scale of 1 to 5, how confident are you?". Those questions produce information.
  3. Make the state visible so you don't depend on the answer: the board, the draft PR, the handoff note. On Tuesday the PR would have shown no contract tests yet.
  4. Agree what happens if it's at risk ("tell me by Wednesday 19:00 IST and we'll demo the UI against the mock") so raising it early is the expected, safe move, and thank people when they raise risks.
  5. Don't conclude "they're unreliable". The process failed to surface the risk; fix the process first.

#Keeping quality consistent across locations

Added

One team, one standard: the quality bar must not depend on which site wrote the code or which site reviewed it. The failure mode is subtle. Each site develops its own habits (one writes tests, the other relies on manual checks; one squashes, the other doesn't), reviews flow one way ("Austin approves India's PRs"), and over a year the codebase grows two styles and a quiet hierarchy. Quality across locations comes from shared definitions enforced by tooling, and from reviews that flow in both directions.

The mental model is one recipe, two kitchens. A restaurant chain with kitchens in two cities doesn't rely on the chefs remembering how the dish should taste. It writes down the recipe, uses the same ingredients and the same equipment, and tastes from both kitchens. The written recipe is the Definition of Done and the standards; the equipment is CI; tasting from both kitchens is cross-site review.

A good way to make "the same standard" concrete is to check the things people forget, in CI, on every PR, from both sites. This workflow fails a PR whose description is missing the sections the team's PR template requires (actionlint 1.7.12 reports no problems for it):

.github/workflows/pr-description.yml
name: PR description

on:
  pull_request:
    types: [opened, edited, synchronize, reopened]

permissions:
  contents: read

jobs:
  sections:
    runs-on: ubuntu-latest
    steps:
      - name: Check the description has the shared sections
        env:
          BODY: ${{ github.event.pull_request.body }}
        run: |
          missing=0
          for section in "## What and why" "## How I tested it" "## Risks and rollback"; do
            if ! grep -qF "$section" <<< "$BODY"; then
              echo "::error::PR description is missing the section: $section"
              missing=1
            fi
          done
          exit "$missing"
What’s happening
  1. It runs on pull_request events, including edited, so fixing the description re-runs the check without a new commit. permissions: contents: read gives the job the least access it needs.
  2. The PR body is passed in through an environment variable (BODY), not pasted into the script with ${{ }}. A PR description is text anyone opening a PR controls; inserting it into a shell script directly would let a crafted description run commands. actionlint flags the direct form (shown below).
  3. The loop checks for each required heading with grep -qF (fixed string, quiet) and prints a GitHub ::error:: annotation for each one missing, so the author sees exactly what to add.
  4. exit "$missing" fails the job if anything was missing. Make it a required status check on the main branch and it applies equally to every PR, from every site and every seniority level, including the lead's.
  5. What it enforces is the habit that matters across time zones: a reviewer eleven hours away can review from the description and the diff, because "how I tested it" and "risks and rollback" are always there.
AdvancedThe PR template it checks against
.github/pull_request_template.md
## What and why
<!-- One or two lines: what changes, and the ticket it closes (e.g. Closes API-298). -->

## How I tested it
<!-- Commands run, tests added, devices or simulator images and firmware versions used. -->

## Risks and rollback
<!-- What could break, who's affected, how to roll back (revert, feature flag off). -->

## Notes for the reviewer
<!-- Where to start reading; anything you want a second opinion on. Your time zone if you need an answer by a time. -->
What’s happening
  1. GitHub pre-fills every new PR's description from .github/pull_request_template.md, so the sections the workflow checks are already there; the author fills them in.
  2. How I tested it asks for firmware versions and simulator images, because on a device platform "works on my device" is the most common gap between sites with different lab hardware.
  3. Risks and rollback forces the author to think about failure before merge, which is the quality habit that's hardest to review for after the fact.
  4. Notes for the reviewer is optional (the workflow doesn't require it) and is where cross-site reviews get faster: "start with handler.ts; I'm unsure about the retry logic; I need an answer by 11:00 IST".
AdvancedWhat actionlint says about the unsafe version

The tempting one-liner puts the PR body straight into the script:

Don't: untrusted input inside run
      - run: echo "${{ github.event.pull_request.body }}" | grep -qF "## How I tested it"

actionlint 1.7.12 rejects it. Real output, with the step saved as line 7 of a workflow called bad.yml:

actionlint output
.github/workflows/bad.yml:7:24: "github.event.pull_request.body" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details [expression]
  |
7 |       - run: echo "${{ github.event.pull_request.body }}" | grep -qF "## How I tested it"
  |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

GitHub expands ${{ }} before the shell runs, so a PR description containing "; curl … | sh; echo " becomes part of the script. Through an environment variable, the shell sees it only as data. The same applies to issue titles, branch names and commit messages.

AdvancedWhat one-way review costs

"Austin reviews everything" sounds like a quality decision; it's also a scheduling decision. This model measures, for a few illustrative PRs, the time from opening a PR to the author being able to act on the first review, with Austin-only reviewers and with owners at both sites:

review-wait.mjs
// How long does a PR wait for its first review? Austin-only reviewers vs owners on both sites.
// A reviewer picks a PR up after 60 of their own working minutes (they finish what they're doing first).
// Austin works 08:00–17:00 America/Chicago, India 11:00–20:00 Asia/Kolkata, Monday–Friday.
const sites = {
  austin: { zone: "America/Chicago", start: 8 * 60, end: 17 * 60 },
  india: { zone: "Asia/Kolkata", start: 11 * 60, end: 20 * 60 },
};
const working = (site, t) => {
  const p = Object.fromEntries(
    new Intl.DateTimeFormat("en-US", { timeZone: sites[site].zone, weekday: "short", hour: "numeric", minute: "numeric", hourCycle: "h23" })
      .formatToParts(t).map((x) => [x.type, x.value]),
  );
  const m = Number(p.hour) * 60 + Number(p.minute);
  return p.weekday !== "Sat" && p.weekday !== "Sun" && m >= sites[site].start && m < sites[site].end;
};
// Minute by minute until some reviewer in the pool has spent 60 working minutes since the PR opened.
function firstReview(opened, pool) {
  const spent = Object.fromEntries(pool.map((s) => [s, 0]));
  for (let t = opened.getTime(); ; t += 60_000) {
    for (const s of pool) if (working(s, new Date(t)) && ++spent[s] >= 60) return { at: new Date(t + 60_000), by: s };
  }
}
const nextWorking = (site, t) => {
  let x = t.getTime();
  while (!working(site, new Date(x))) x += 60_000;
  return new Date(x);
};
const show = (t, site) =>
  new Intl.DateTimeFormat("en-GB", { timeZone: sites[site].zone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(t);

const prs = [ // illustrative PRs in the week of 2 November 2026 (US standard time)
  ["India dev, Tue 12:00 IST", "2026-11-03T12:00:00+05:30", "india"],
  ["India dev, Tue 16:00 IST", "2026-11-03T16:00:00+05:30", "india"],
  ["India dev, Fri 15:00 IST", "2026-11-06T15:00:00+05:30", "india"],
  ["Austin dev, Tue 14:00 CT", "2026-11-03T14:00:00-06:00", "austin"],
];
for (const [policy, pool] of [["Austin reviews everything", ["austin"]], ["Owners on both sites", ["austin", "india"]]]) {
  console.log(policy);
  let total = 0;
  for (const [name, iso, author] of prs) {
    const opened = new Date(iso);
    const { at, by } = firstReview(opened, pool);
    const acts = nextWorking(author, at); // when the author, in their own day, can act on the comments
    const hours = (acts - opened) / 3_600_000;
    total += hours;
    console.log(`  ${name.padEnd(25)} → reviewed ${show(at, author)} by ${by.padEnd(6)} → author acts ${show(acts, author)}  ${hours.toFixed(1).padStart(4)} h`);
  }
  console.log(`  average from opening to acting on the review: ${(total / prs.length).toFixed(1)} h`);
}
Result
Austin reviews everything
  India dev, Tue 12:00 IST  → reviewed Tue 20:30 by austin → author acts Wed 11:00  23.0 h
  India dev, Tue 16:00 IST  → reviewed Tue 20:30 by austin → author acts Wed 11:00  19.0 h
  India dev, Fri 15:00 IST  → reviewed Fri 20:30 by austin → author acts Mon 11:00  68.0 h
  Austin dev, Tue 14:00 CT  → reviewed Tue 15:00 by austin → author acts Tue 15:00   1.0 h
  average from opening to acting on the review: 27.8 h
Owners on both sites
  India dev, Tue 12:00 IST  → reviewed Tue 13:00 by india  → author acts Tue 13:00   1.0 h
  India dev, Tue 16:00 IST  → reviewed Tue 17:00 by india  → author acts Tue 17:00   1.0 h
  India dev, Fri 15:00 IST  → reviewed Fri 16:00 by india  → author acts Fri 16:00   1.0 h
  Austin dev, Tue 14:00 CT  → reviewed Tue 15:00 by austin → author acts Tue 15:00   1.0 h
  average from opening to acting on the review: 1.0 h
What’s happening
  1. A reviewer picks a PR up after spending 60 of their own working minutes (finishing what they were doing first), and the author can act on the comments only in their own working day. Both sites keep their normal hours; it's the week of 2 November 2026, US standard time.
  2. With Austin-only review, an India PR opened at 12:00 IST on Tuesday is reviewed at 20:30 IST (09:00 Austin), half an hour after the India day ended. The author acts on it at 11:00 IST on Wednesday: 23 hours for one review round.
  3. The Friday PR is reviewed at 20:30 IST on Friday and acted on at 11:00 IST on Monday: 68 hours. Each further round of review adds roughly another day.
  4. With owners on both sites, every PR here gets its first review within an hour, from a reviewer in the author's own working day. The Austin PR is the same in both policies (at 14:00 CT, India is asleep).
  5. The model is generous to both policies (reviewers always available, one review round). Real numbers are worse, which is why the review flow, not the code, often explains "India's PRs take longer". The fix is area owners at both sites, and the lead reviewing selectively, not universally.
AdvancedShared definitions and two-way reviews
  • One Definition of Done for the whole team, written in the repository and the same for both sites (see Definition of done and quality gates). If the device lab hardware differs by site, the DoD names the simulator images or firmware versions every change must be tested on, so "tested" means the same thing everywhere.
  • Reviews in both directions. India-based engineers review Austin's PRs, including the lead's. One-way review creates a hierarchy, slows India's PRs by a day each (they wait for the Austin morning), and means Austin's code gets fewer eyes. A CODEOWNERS file can assign owners per area with at least one owner at each site, so a PR always has an available reviewer in its author's working day:
.github/CODEOWNERS
# Each area has owners on both sites, so a PR always has a reviewer awake.
/web/devices/      @acme/devices-austin @acme/devices-india
/api/settings/     @acme/api-austin @acme/api-india
/.github/          @acme/tech-leads
  • Measure the system, not the sites. Track review turnaround, escaped defects and CI health for the team. Comparing sites ("India's PRs have more review comments") turns quality into a contest and is usually explained by who reviews whom. If one area has more problems, look at its ownership, tests and docs.
  • Rotate ownership of the quality work: who fixes flaky tests, who updates the lint rules, who runs the retro. Quality culture belongs to the team, not to the lead's site.
AdvancedTrap: "Austin approves India's PRs"
Two kitchens, one recipe

After six months, defects in production are noticeably higher in the device-settings area, which is mostly built in India, than in the device-list area, mostly built in Austin. A manager suggests adding a mandatory Austin review for all India PRs. What do you say, and what do you check first?

Show answer

Don't add a site-based gate. Investigate the area, not the site: tests, ownership, documentation and review flow. Then fix the cause with shared tooling.

What’s happening
  1. A site gate would slow every India PR by up to a day, signal distrust, and wouldn't fix the cause if the cause is something else, such as missing contract tests or undocumented device behaviour.
  2. Check the area's tests: does device-settings have contract tests against each supported firmware version? Settings write to the device, so firmware differences cause bugs that unit tests miss.
  3. Check the inputs: is the device's settings API documented, or are developers working from assumptions? (Back to the assumption log from Shifting priorities and incomplete documentation.) Check the review flow too: who reviews device-settings PRs, and do they have the expertise?
  4. Fix with shared mechanisms: add the contract tests to the Definition of Done for that area and make them a required check, add an owner with device expertise on each site in CODEOWNERS, and write down the device-API notes. Then measure escaped defects for that area again in two months.
  5. Say it in terms the manager cares about: "a site gate costs a day per PR and doesn't target the cause; contract tests and area ownership do, and they protect Austin's changes to that area too".

Written fresh for a lead full stack role. Git examples were run; GitHub Actions workflows were checked with actionlint. Behavioural stories are yours to write in My notes.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.