Risk Management in Software Projects: Pre-Mortem, Risk Register, Probability × Impact and Retiring Risk Early
Managing risk means surfacing it early with a pre-mortem, comparing probability × impact with the buffer and putting the riskiest work at the start of the plan. In Bookstore's Institutional Orders register, total exposure is 21.6 days, more than the scope buffer; an ERP spike and pair work bring it down to 4.9 days in three sprints. We cover ROAM, risk burndown and risk vs issue with C# code.
Version note: The risk register and risk burndown code in this post were built and run with .NET 10; the pre-mortem part is based on Gary Klein's Performing a Project Premortem (Harvard Business Review, 2007).
Make a Guess First
Friday, 20 November, the last hour before Sprint 24. The team of Bookstore, the online bookstore used as the shared example across this site's series, is in the meeting room. Selin hands everyone a blank sheet and says: "It's the morning of March 1. Nevin Hanım's school couldn't order through the portal, Gül is typing in lists by phone all day and Hakan is about to lose the school. What happened? You have five minutes; write down every reason you can think of."
Write yours too before reading on. What is the most likely reason this project misses March 1? Which reason is the most expensive, the one that costs the most days if it happens? Are they the same? And think about this: if you already know two people will be on leave in the New Year week, is that a risk, or something else?
Risk vs Issue
A risk is an uncertain event that has not happened yet but would hurt the plan if it did; it has a probability and an impact. An issue is a problem that has already happened; it has no probability, only an impact and work to resolve it. The distinction matters because the two are managed differently. For a risk the question is "how do we reduce its probability or impact?"; for an issue it is "what do we do now?"
The answer to the question above lies in this distinction. If the leave calendar is fixed, the capacity drop in the New Year week is not a risk but a fact: it should go straight into the plan as capacity. What stays a risk is the uncertain part, how much leave there will be or who else falls ill. A risk that materializes leaves the register, becomes an issue and moves into the plan; we will see that in the burndown below.
In a software project, risks come in a few kinds. Technical risks are about something the team hasn't done yet turning out to be hard: for Institutional Orders, the format of the daily invoice report for the ERP that accounting uses is unknown. Dependency risks come from relying on another team's work: the warehouse team has to add the delivery point field to its system. People risks come from knowledge concentrated in one person; the measure for this is the bus factor, how many people would have to leave before the project stalls. Scope risks are about something left out today coming back. Regulatory risks come from legal obligations: the fields required on invoices issued to schools, and the privacy notice and consent text required under KVKK, Turkey's personal data protection law, for institution users' personal data.
Pre-Mortem: An Autopsy Before the Project Ends
What Selin is doing is called a pre-mortem. In the technique Gary Klein described in Harvard Business Review in 2007, the team assumes the project it is planning has failed and generates the reasons. A normal risk meeting asks "what could go wrong?"; a pre-mortem asks "it went wrong, why?" The difference looks small, but its effect is large. Once failure is on the table as an assumption, the person who doubts the plan is not opposing anyone, just doing the assigned task. According to Klein, that is exactly the purpose: making it easier, during planning, for people who know the plan's weak spots but hesitate to say so to speak up.
The session itself is simple. The team knows the plan. The facilitator announces that the project has failed completely. For a few minutes everyone writes down every reason they can think of, without talking to anyone. Then each person in turn names one reason from their list and it goes on the whiteboard; the rounds continue until the lists are exhausted. The silent writing step does the same job as turning cards over together in planning poker: everyone's list exists before the most senior person's opinion is heard. Some teams let the leader go first as an example; a leader naming a weak spot in their own plan makes it easier for the rest of the room to be candid.
Close to twenty reasons pile up on Bookstore's whiteboard; some are the same thing said differently. Merged, seven risks remain: the ERP report format, the extra fields on school invoices, only Emre knowing the pricing code, the warehouse team's delivery point work, the approval flow being pulled back into the first release, the consent text for institution users and a 500-row list being slow when pasted. Compared with the "forgotten work" list from the first post, most of them look familiar: same team, same kind of surprises.
Risk Register: Probability × Impact
A pre-mortem produces a list; a risk register makes that list manageable. For each risk you keep at least these fields: a short description, its kind, its owner, its probability, its impact if it happens, a trigger that shows it is about to happen and the planned response. The owner is not the person who personally fixes the risk, but the one who watches it and raises the alarm when needed.
The common way to rank risks is exposure: probability times impact. If impact is given in days, exposure is in days too and can be compared directly with the buffer in the plan. Bookstore's register:
namespace Bookstore.Planning;
public enum RiskKind { Technical, Dependency, People, Scope, Regulation }
// ProbabilityBySprint[0] = right after the pre-mortem, [1] = end of Sprint 24, [2] = end of Sprint 25, [3] = end of Sprint 26
public sealed record Risk(string Id, string Title, RiskKind Kind, string Owner, int ImpactDays,
double[] ProbabilityBySprint, string Trigger, string Response)
{
public double Exposure(int review) => ProbabilityBySprint[review] * ImpactDays;
}
public static class RiskRegister
{
static readonly string[] Reviews = ["start", "S24", "S25", "S26"];
static readonly Risk[] Risks =
[
new("R1", "ERP export format unknown", RiskKind.Technical, "Emre", 15, [0.5, 0.15, 0.1, 0.1],
"No ERP test endpoint by day 5 of Sprint 24", "2-day spike in Sprint 24"),
new("R2", "E-invoice fields for institutions", RiskKind.Regulation, "Selin", 10, [0.3, 0.3, 0.1, 0.05],
"Accountant has not confirmed the fields", "Meeting with accounting in Sprint 25"),
new("R3", "Only Emre knows the pricing code", RiskKind.People, "Ayse", 10, [0.25, 0.15, 0.1, 0.1],
"Emre on leave or sick", "Pair on every pricing story"),
new("R4", "Warehouse team adds delivery points", RiskKind.Dependency, "Selin", 8, [0.4, 0.4, 0.2, 0],
"Field not in their sprint plan", "Agree the API contract, check every week"),
new("R5", "Approval flow pulled into Release 1", RiskKind.Scope, "Selin", 12, [0.3, 0.3, 0.1, 0.1],
"School asks for in-app approval", "Hakan confirms with the school"),
new("R6", "Consent text for institution users", RiskKind.Regulation, "Selin", 5, [0.2, 0.2, 0.2, 0.1],
"Legal review not booked", "Book legal review early"),
new("R7", "500-row list paste too slow", RiskKind.Technical, "Ayse", 4, [0.2, 0.05, 0.05, 0.05],
"p95 above 2 s in test", "Load test the parser in Sprint 24"),
new("R8", "Year-end leave cuts capacity", RiskKind.People, "Selin", 4, [0, 0, 0.6, 0],
"Leave calendar shows overlaps", "Plan smaller Sprint 26"),
];
public static void Run()
{
Console.WriteLine($"{"Id",-3} {"Risk",-38} {"Kind",-11} {"P",5} {"Days",4} {"Exp",5} Owner");
foreach (Risk r in Risks.Where(r => r.ProbabilityBySprint[0] > 0).OrderByDescending(r => r.Exposure(0)))
Console.WriteLine($"{r.Id,-3} {r.Title,-38} {r.Kind,-11} {r.ProbabilityBySprint[0],5:F2} {r.ImpactDays,4} {r.Exposure(0),5:F1} {r.Owner}");
Console.WriteLine($"Total exposure at start: {Risks.Sum(r => r.Exposure(0)):F1} days");
// Risk burndown: total exposure at each review
Console.WriteLine();
Console.WriteLine("Risk burndown (total exposure, days)");
for (int i = 0; i < Reviews.Length; i++)
{
double total = Risks.Sum(r => r.Exposure(i));
Console.WriteLine($" {Reviews[i],-5} {total,5:F1} {new string('#', (int)Math.Round(total))}");
}
Risk top = Risks.MaxBy(r => r.Exposure(0))!;
Console.WriteLine();
Console.WriteLine($"Top risk {top.Id}: trigger \"{top.Trigger}\", response \"{top.Response}\"");
}
}
The ProbabilityBySprint array keeps each risk's probability at the risk review at the end of every sprint, so the register's history is not lost. We add case "risk": RiskRegister.Run(); break; to Program.cs and run it:
dotnet run --project src/Bookstore.Planning -- risk
Id Risk Kind P Days Exp Owner
R1 ERP export format unknown Technical 0.50 15 7.5 Emre
R5 Approval flow pulled into Release 1 Scope 0.30 12 3.6 Selin
R4 Warehouse team adds delivery points Dependency 0.40 8 3.2 Selin
R2 E-invoice fields for institutions Regulation 0.30 10 3.0 Selin
R3 Only Emre knows the pricing code People 0.25 10 2.5 Ayse
R6 Consent text for institution users Regulation 0.20 5 1.0 Selin
R7 500-row list paste too slow Technical 0.20 4 0.8 Ayse
Total exposure at start: 21.6 days
Risk burndown (total exposure, days)
start 21.6 ######################
S24 14.7 ###############
S25 9.9 ##########
S26 4.9 #####
Top risk R1: trigger "No ERP test endpoint by day 5 of Sprint 24", response "2-day spike in Sprint 24"
Reading the Output
After the pre-mortem, total exposure is 21.6 days. Let's compare that with the plan from the fourth post. In the average case Release 1 finished only three days before March 1; there is almost no slack in the calendar. The real buffer was in scope: the Should and Could items total 34 points, 1.7 sprints at average velocity, roughly 17 working days. The expected cost of the risks is bigger than that buffer. If we do nothing, the expected part of the risks alone eats all of the Should items and some of the Coulds.
At the top of the ranking is R1, the ERP report format: 50% probability, 15 days of impact, 7.5 days of exposure. On its own it is more than a third of the total. The two questions from Make a Guess First split here: the most likely risk is R4, the warehouse team at 40%, while the most expensive is R1 at 15 days. Exposure lets you weigh both on the same scale.
Exposure has a limit, too: it is an expected value. A risk with 5% probability and 40 days of impact disappears in the middle of the list with 2 days of exposure, yet it would end the release if it happened. So when you look at the register, look not only at the exposure ranking but also, separately, at risks whose impact exceeds the whole buffer. At Bookstore, R1's 15 days is almost the entire scope buffer.
Probability and impact are estimates as well and carry all the problems from the first post. The goal here is not a precise number but comparison: to say R1 matters ten times more than R7, the probabilities don't need to be right to the second digit. Teams often give probability in coarse classes such as low, medium and high; turning them into numbers is only for comparing total exposure with the buffer.
Classifying with ROAM
In large planning meetings, ROAM is used to classify risks quickly; it spread with SAFe, the Scaled Agile Framework, which coordinates several agile teams around a shared plan. Each risk goes into one of four boxes. Resolved is a risk that has been dealt with and no longer threatens delivery. Owned is a risk someone has taken on and is watching. Accepted is a risk the team knowingly accepts and will do nothing more about. Mitigated is a risk with an action plan to reduce its impact or probability.
The benefit of ROAM is that every risk must get a decision. "Noted" is not a category. At Bookstore, R7, the slow list, is Mitigated: a load test is planned in Sprint 24. R5, the approval flow, is Owned: Hakan will talk to the school and bring back the answer. R6, the consent text, looks Accepted at first, but it actually depends on the legal review's schedule; it should be Owned. This classification is like a summary of the register's response column; not a separate tool.
Turning Risk into Plan
A risk register that does not change the plan is just a document. The responses to a risk come in roughly four kinds: avoiding it by removing the risky scope, reducing it by lowering its probability or impact, transferring it to someone else such as a supplier, and accepting it. In software teams the most effective is a form of reduction: doing the riskiest work first.
Bookstore's plan changes accordingly. The first item of Sprint 24 is a two-day spike for the ERP report: get a sample file from accounting and try importing it into the test environment. If the spike succeeds, R1's probability drops; if it fails, the team learns that ten weeks before March 1 and can discuss scope or date now. Both are a win. In the same sprint there is a load test for the 500-row list, and the pricing stories go to Emre and Ayşe as pair work, raising the bus factor from 1 to 2.
This approach has two other names. A walking skeleton is the thinnest first version that connects all layers of the system end to end; the skeleton we chose for Institutional Orders in the Requirements Analysis series post "Story Mapping and Splitting Stories" was there to make integration risks visible in the first sprint. The spiral model is a development cycle where every round starts with the biggest risk; we covered it in the SDLC series post "Incremental, Iterative and Spiral Models: Surfacing Risk Early". Both share one idea: don't leave uncertainty for the end of the calendar, put it at the start.
This also changes something on the burn-up from the fourth post. Pulling risky work forward can lower velocity a little in the first sprints, because spikes produce no points. But as risk goes down, the bad end of the range is pulled up. The release plan and the risk register are not separate documents; one shows the date, the other shows what threatens it.
Risk Burndown
To see risk going down, you can use the risk burndown chart that Mike Cohn describes in his post and that John Brothers introduced in The Agile Times in 2004. The idea is simple: the total exposure of the risks in the register is updated at every Sprint Planning and tracked as a line chart. Cohn recommends summing only the top ten risks even if the list grows, so the line doesn't rise artificially because new small risks were added to the register.
The second block of the output shows how Bookstore's register changed over three sprints. Here we jump ahead a little: these are the values you see when looking back at the register's history on January 4, after Sprint 26 ended. Total exposure falls from 21.6 days to 4.9. The biggest drop is in Sprint 24: the ERP spike succeeded and R1's probability went from 50% to 15%. In Sprint 25 a meeting with accounting clarified the fields on school invoices and the warehouse team took the delivery point field into its own sprint.
There is one more important detail the chart doesn't show. At the Sprint 25 review a new risk was added: R8, overlapping New Year leave. The total still fell because the other risks were shrinking faster. In Sprint 26 R8 left the register because it was no longer a risk: the leave happened and that sprint's velocity dropped. It turned into an issue and will show up in the plan itself. Another detail is the fate of R2 and R6: the school invoice fields and the consent text are no longer uncertain, but new stories took the place of the uncertainty. Risk went down, scope went up. We will see that in the scope line of the burn-up in the sixth post.
In Production
Tie the register to a rhythm. The easiest way is to make the risk review a fixed item of Sprint Planning: ten minutes, only risks whose probability or impact changed and new risks. Risk reviews scheduled as separate meetings get cancelled in the first busy week.
Write concrete triggers. "The ERP integration may cause problems" is not a trigger; "no access to the ERP test environment by day five of Sprint 24" is. A concrete trigger removes any debate about when the owner should raise the alarm.
Keep the register where everyone can see it. Keeping risks only in the Product Owner's file closes the conversation the pre-mortem opened. How to keep a register in an issue tracker is covered in the Jira and Project Tools series; but a one-page table is enough.
Common Pitfalls
- Writing the risk list once and putting it in a drawer: a register that isn't updated loses touch with reality within a month.
- Marking every risk "low": small probabilities make the plan look comfortable but don't change the risk itself; the doubt the pre-mortem opened closes again.
- Risks without owners: "the team" is not an owner; one person must watch it and raise the alarm.
- Mixing up risks and issues: keeping something certain as a risk hides capacity that should be in the plan.
- Looking only at exposure: low-probability but devastating risks get lost in the middle of the list.
- Not putting the response into the plan: if the spike, load test or pair work is not an item in the backlog, it doesn't happen.
A Small Experiment
★☆☆ Run a fifteen-minute pre-mortem for the project you are working on: even on your own, assume the project failed and write five reasons. How many of them are tied to an action in your current plan?
★★☆ Add a block to RiskRegister that separately lists risks whose impact exceeds a threshold, such as 10 days, regardless of the exposure ranking. Which Bookstore risks make that list?
★★★ Add a RoamStatus enum to the Risk record and keep its status at every review. Then write a variant that computes the risk burndown only from Owned and Mitigated risks. How does removing Accepted risks from the total change the chart, and why might that be misleading?
The risk register shows risk going down, but the last paragraph ended with a warning: capacity dropped and scope grew. In the next post we move to January 4: reading a slipping plan from the burn-up, delivering bad news early and choosing between cutting scope, moving the date or adding people.