Why Estimation Is Hard (and Important)
Software estimation is notoriously difficult because software development is creative, knowledge-intensive work with high variability. Unlike manufacturing, where you can measure how long it takes to produce identical items, every software feature is unique. Yet estimation remains essential for planning, budgeting, staffing, and setting stakeholder expectations.
The goal of estimation is not to predict the future perfectly — it is to be roughly right rather than precisely wrong, and to improve accuracy over time through data and feedback.
The Cone of Uncertainty
Early in a project, estimates can be off by 4x in either direction. As the project progresses and uncertainty decreases, estimates become more accurate:
- Initial Concept: 0.25x to 4x (400% variance)
- After Requirements: 0.5x to 2x (200% variance)
- After Design: 0.67x to 1.5x (150% variance)
- After Implementation Started: 0.8x to 1.25x (125% variance)
Estimation Techniques Comparison
| Technique | Type | Accuracy | Effort | Best For |
|---|---|---|---|---|
| Story Points | Relative | Medium-High | Low | Sprint-level planning |
| Planning Poker | Relative | Medium-High | Medium | Team consensus estimation |
| T-Shirt Sizing | Relative | Low-Medium | Very Low | Roadmap / epic-level planning |
| Three-Point (PERT) | Absolute | Medium-High | Medium | Tasks with high uncertainty |
| Analogous | Absolute | Low-Medium | Low | Early-stage, similar past projects |
| Parametric | Absolute | Medium | Low | Repeatable work with known rates |
| Bottom-Up | Absolute | High | High | Detailed planning, budgeting |
Story Points and the Fibonacci Scale
Story points measure the relative effort of a user story, considering complexity, uncertainty, and volume of work. Teams use a modified Fibonacci sequence (1, 2, 3, 5, 8, 13, 21) because larger items have more uncertainty and the gaps between numbers reflect that.
// Estimation models and calculators
interface EstimationResult {
technique: string;
estimate: number;
unit: 'points' | 'hours' | 'days';
confidence: number; // percentage
range: { low: number; high: number };
}
// Three-Point (PERT) Estimation
function pertEstimate(
optimistic: number,
mostLikely: number,
pessimistic: number
): { expected: number; standardDeviation: number; range95: { low: number; high: number } } {
// PERT formula: (O + 4M + P) / 6
const expected = (optimistic + 4 * mostLikely + pessimistic) / 6;
// Standard deviation: (P - O) / 6
const standardDeviation = (pessimistic - optimistic) / 6;
// 95% confidence interval: expected ± 2 * SD
return {
expected: Math.round(expected * 10) / 10,
standardDeviation: Math.round(standardDeviation * 10) / 10,
range95: {
low: Math.round((expected - 2 * standardDeviation) * 10) / 10,
high: Math.round((expected + 2 * standardDeviation) * 10) / 10
}
};
}
// Example: Estimate for building a search feature
const searchFeatureEstimate = pertEstimate(
3, // Optimistic: 3 days (simple Elasticsearch setup)
7, // Most Likely: 7 days (standard implementation)
15 // Pessimistic: 15 days (unexpected data issues)
);
// Result: expected = 7.3 days, SD = 2.0, 95% range = 3.3 - 11.3 days
// Velocity-based Sprint Forecasting
interface SprintForecast {
sprintNumber: number;
velocity: number; // story points completed
}
function forecastCompletion(
historicalVelocity: SprintForecast[],
remainingPoints: number
): { sprintsNeeded: number; expectedDate: Date; range: { best: number; worst: number } } {
const velocities = historicalVelocity.map(s => s.velocity);
const avgVelocity = velocities.reduce((a, b) => a + b, 0) / velocities.length;
const minVelocity = Math.min(...velocities);
const maxVelocity = Math.max(...velocities);
const sprintsNeeded = Math.ceil(remainingPoints / avgVelocity);
const bestCase = Math.ceil(remainingPoints / maxVelocity);
const worstCase = Math.ceil(remainingPoints / minVelocity);
const today = new Date();
const expectedDate = new Date(today.getTime() + sprintsNeeded * 14 * 24 * 60 * 60 * 1000);
return { sprintsNeeded, expectedDate, range: { best: bestCase, worst: worstCase } };
}
// Example
const velocity: SprintForecast[] = [
{ sprintNumber: 1, velocity: 28 },
{ sprintNumber: 2, velocity: 34 },
{ sprintNumber: 3, velocity: 31 },
{ sprintNumber: 4, velocity: 36 },
{ sprintNumber: 5, velocity: 33 },
];
const forecast = forecastCompletion(velocity, 120);
// avg velocity = 32.4, sprints needed = 4, range: 4-5 sprints
// Planning Poker Simulation
interface PlanningPokerRound {
storyId: string;
votes: { member: string; points: number }[];
consensus: number | null;
discussion: string;
}
function analyzePlanningPokerVotes(
votes: { member: string; points: number }[]
): { median: number; spread: number; needsDiscussion: boolean } {
const points = votes.map(v => v.points).sort((a, b) => a - b);
const median = points[Math.floor(points.length / 2)];
const spread = Math.max(...points) - Math.min(...points);
// If spread is more than 2 Fibonacci numbers apart, discussion needed
const fibSequence = [1, 2, 3, 5, 8, 13, 21];
const minIdx = fibSequence.indexOf(Math.min(...points));
const maxIdx = fibSequence.indexOf(Math.max(...points));
const needsDiscussion = maxIdx - minIdx > 2;
return { median, spread, needsDiscussion };
}
// T-Shirt to Points Mapping
const tshirtToPoints: Record = {
'XS': { points: 1, days: '< 1 day' },
'S': { points: 2, days: '1-2 days' },
'M': { points: 5, days: '3-5 days' },
'L': { points: 8, days: '1-2 weeks' },
'XL': { points: 13, days: '2-3 weeks' },
'XXL': { points: 21, days: 'Split this story!' },
};
Estimation Best Practices
Rules for Better Estimates
- Estimate as a Team: Group estimates are more accurate than individual ones. Use Planning Poker for consensus.
- Use Relative Sizing: Humans are better at comparing ("this is twice as complex as that") than absolute estimates ("this will take 14 hours").
- Track Velocity: Use historical data to improve forecasting. After 3-5 sprints, velocity stabilizes.
- Estimate Uncertainty, Not Just Effort: A 5-point story with low uncertainty is very different from a 5-point story with high uncertainty.
- Split Large Items: Stories larger than 13 points should be split. Smaller stories have more predictable delivery.
- Never Pad Silently: If you need a buffer, add it explicitly as a contingency, not hidden in individual estimates.
When Estimates Go Wrong
Common causes of estimation failures and how to address them:
- Anchoring Bias: First estimate influences all others. Let everyone vote simultaneously (Planning Poker).
- Optimism Bias: Developers consistently underestimate. Use historical data to calibrate.
- Scope Creep: Requirements grow but estimates do not update. Re-estimate when scope changes.
- Missing Tasks: Estimates forget testing, documentation, deployment, and code review. Include these explicitly.