Postmortem Template in Java: A Structured Approach to Incident Analysis

While postmortems are typically document-based, we can create a Java framework to structure, automate, and standardize the postmortem process. This approach ensures consistency and enables data analysis across multiple incidents.


Core Postmortem Model

1. Main Postmortem Class
package com.company.postmortem.model;
import java.time.LocalDateTime;
import java.util.*;
public class Postmortem {
private String id;
private String title;
private IncidentSeverity severity;
private PostmortemStatus status;
private LocalDateTime incidentStartTime;
private LocalDateTime incidentEndTime;
private LocalDateTime detectionTime;
private LocalDateTime resolutionTime;
private LocalDateTime postmortemCreatedTime;
private LocalDateTime postmortemPublishedTime;
// Core sections
private IncidentSummary summary;
private ImpactAssessment impact;
private Timeline timeline;
private RootCauseAnalysis rootCauseAnalysis;
private ActionItems actionItems;
private LessonsLearned lessonsLearned;
private List<String> contributors;
private String leadInvestigator;
private Map<String, String> metadata;
// Constructors, getters, setters
public Postmortem() {
this.id = UUID.randomUUID().toString();
this.status = PostmortemStatus.DRAFT;
this.postmortemCreatedTime = LocalDateTime.now();
this.contributors = new ArrayList<>();
this.metadata = new HashMap<>();
}
// Enum definitions
public enum IncidentSeverity {
SEV1, SEV2, SEV3, SEV4
}
public enum PostmortemStatus {
DRAFT, IN_REVIEW, PUBLISHED, CLOSED
}
}
2. Supporting Domain Classes
// Incident Summary
public class IncidentSummary {
private String description;
private String trigger;
private String serviceAffected;
private String componentAffected;
private List<String> detectionMethods;
// constructors, getters, setters
}
// Impact Assessment
public class ImpactAssessment {
private Duration duration;
private Map<String, String> businessImpact; // "Revenue Loss": "$10,000"
private Map<String, String> customerImpact; // "Affected Users": "5,000"
private Map<String, String> technicalImpact; // "Error Rate": "Increased by 15%"
private List<String> affectedSystems;
private List<String> affectedTeams;
public Duration getTotalDowntime() {
return duration;
}
}
// Timeline of Events
public class Timeline {
private List<TimelineEvent> events;
public void addEvent(LocalDateTime timestamp, String description, String source) {
events.add(new TimelineEvent(timestamp, description, source));
}
public List<TimelineEvent> getChronologicalEvents() {
return events.stream()
.sorted(Comparator.comparing(TimelineEvent::getTimestamp))
.toList();
}
}
public class TimelineEvent {
private LocalDateTime timestamp;
private String description;
private String source; // "Monitoring", "On-call", "Customer Report"
private EventType type; // DETECTION, MITIGATION, RESOLUTION, etc.
// constructors, getters, setters
}
// Root Cause Analysis
public class RootCauseAnalysis {
private String primaryRootCause;
private List<String> contributingFactors;
private List<String> triggerEvents;
private Map<String, String> evidence; // "Log Analysis": "Found connection pool exhaustion"
private boolean isHumanError;
private boolean isSystemicIssue;
private List<String> gapsIdentified;
}
// Action Items
public class ActionItem {
private String id;
private String description;
private ActionItemType type;
private String owner;
private LocalDateTime dueDate;
private ActionItemStatus status;
private Priority priority;
private List<String> dependencies;
private String verificationMethod;
public enum ActionItemType {
PREVENT_RECURRENCE,
IMPROVE_DETECTION,
IMPROVE_MITIGATION,
IMPROVE_DOCUMENTATION,
PROCESS_IMPROVEMENT
}
public enum ActionItemStatus {
OPEN, IN_PROGRESS, COMPLETED, VERIFIED, BLOCKED
}
public enum Priority {
HIGH, MEDIUM, LOW
}
}
public class ActionItems {
private List<ActionItem> items;
private LocalDateTime followUpDate;
private String followUpOwner;
public List<ActionItem> getOpenItems() {
return items.stream()
.filter(item -> item.getStatus() != ActionItemStatus.COMPLETED)
.toList();
}
public List<ActionItem> getHighPriorityItems() {
return items.stream()
.filter(item -> item.getPriority() == ActionItem.Priority.HIGH)
.toList();
}
}
// Lessons Learned
public class LessonsLearned {
private String whatWentWell;
private String whatWentWrong;
private String whatCouldBeImproved;
private List<String> positiveFindings;
private List<String> improvementOpportunities;
private List<String> recognition; // Team members who did well
}

Postmortem Builder and Manager

1. Postmortem Builder
package com.company.postmortem.builder;
public class PostmortemBuilder {
private Postmortem postmortem;
public PostmortemBuilder() {
this.postmortem = new Postmortem();
}
public PostmortemBuilder withBasicInfo(String title, Postmortem.IncidentSeverity severity) {
postmortem.setTitle(title);
postmortem.setSeverity(severity);
return this;
}
public PostmortemBuilder withTiming(LocalDateTime start, LocalDateTime end, 
LocalDateTime detection, LocalDateTime resolution) {
postmortem.setIncidentStartTime(start);
postmortem.setIncidentEndTime(end);
postmortem.setDetectionTime(detection);
postmortem.setResolutionTime(resolution);
return this;
}
public PostmortemBuilder withSummary(String description, String trigger, String service) {
IncidentSummary summary = new IncidentSummary();
summary.setDescription(description);
summary.setTrigger(trigger);
summary.setServiceAffected(service);
postmortem.setSummary(summary);
return this;
}
public PostmortemBuilder withImpact(Duration duration, Map<String, String> businessImpact) {
ImpactAssessment impact = new ImpactAssessment();
impact.setDuration(duration);
impact.setBusinessImpact(businessImpact);
postmortem.setImpact(impact);
return this;
}
public PostmortemBuilder withContributors(String lead, List<String> contributors) {
postmortem.setLeadInvestigator(lead);
postmortem.setContributors(contributors);
return this;
}
public Postmortem build() {
validatePostmortem();
return postmortem;
}
private void validatePostmortem() {
if (postmortem.getTitle() == null || postmortem.getTitle().trim().isEmpty()) {
throw new IllegalStateException("Postmortem title is required");
}
if (postmortem.getIncidentStartTime() == null) {
throw new IllegalStateException("Incident start time is required");
}
}
}
2. Postmortem Manager
package com.company.postmortem.manager;
@Component
public class PostmortemManager {
private final PostmortemRepository repository;
private final PostmortemValidator validator;
private final ActionItemTracker actionItemTracker;
public Postmortem createPostmortem(PostmortemBuilder builder) {
Postmortem postmortem = builder.build();
validator.validate(postmortem);
return repository.save(postmortem);
}
public void updateStatus(String postmortemId, Postmortem.PostmortemStatus status) {
Postmortem postmortem = repository.findById(postmortemId)
.orElseThrow(() -> new PostmortemNotFoundException(postmortemId));
postmortem.setStatus(status);
if (status == Postmortem.PostmortemStatus.PUBLISHED) {
postmortem.setPostmortemPublishedTime(LocalDateTime.now());
}
repository.save(postmortem);
}
public void addTimelineEvent(String postmortemId, TimelineEvent event) {
Postmortem postmortem = repository.findById(postmortemId)
.orElseThrow(() -> new PostmortemNotFoundException(postmortemId));
postmortem.getTimeline().addEvent(
event.getTimestamp(), 
event.getDescription(), 
event.getSource()
);
repository.save(postmortem);
}
public void addActionItem(String postmortemId, ActionItem actionItem) {
Postmortem postmortem = repository.findById(postmortemId)
.orElseThrow(() -> new PostmortemNotFoundException(postmortemId));
postmortem.getActionItems().getItems().add(actionItem);
repository.save(postmortem);
// Track the action item separately for follow-up
actionItemTracker.trackActionItem(actionItem);
}
public List<Postmortem> findPostmortemsBySeverity(Postmortem.IncidentSeverity severity) {
return repository.findBySeverity(severity);
}
public List<Postmortem> findOpenPostmortems() {
return repository.findByStatusNot(Postmortem.PostmortemStatus.CLOSED);
}
public Map<Postmortem.IncidentSeverity, Long> getPostmortemStatistics() {
return repository.findAll().stream()
.collect(Collectors.groupingBy(
Postmortem::getSeverity, 
Collectors.counting()
));
}
}

Validation and Analysis

1. Postmortem Validator
@Component
public class PostmortemValidator {
public void validate(Postmortem postmortem) {
List<String> errors = new ArrayList<>();
// Timing validation
if (postmortem.getIncidentStartTime() != null && 
postmortem.getIncidentEndTime() != null &&
postmortem.getIncidentStartTime().isAfter(postmortem.getIncidentEndTime())) {
errors.add("Incident start time cannot be after end time");
}
// Required fields validation
if (postmortem.getSummary() == null || 
postmortem.getSummary().getDescription() == null) {
errors.add("Incident description is required");
}
if (postmortem.getRootCauseAnalysis() == null ||
postmortem.getRootCauseAnalysis().getPrimaryRootCause() == null) {
errors.add("Root cause analysis is required");
}
if (!errors.isEmpty()) {
throw new PostmortemValidationException("Postmortem validation failed: " + 
String.join(", ", errors));
}
}
public void validateActionItemCompletion(Postmortem postmortem) {
if (postmortem.getStatus() == Postmortem.PostmortemStatus.CLOSED) {
long openActionItems = postmortem.getActionItems().getOpenItems().size();
if (openActionItems > 0) {
throw new IllegalStateException(
"Cannot close postmortem with " + openActionItems + " open action items");
}
}
}
}
public class PostmortemValidationException extends RuntimeException {
public PostmortemValidationException(String message) {
super(message);
}
}
2. Root Cause Analyzer
@Component
public class RootCauseAnalyzer {
public RootCauseAnalysis analyze(Timeline timeline, ImpactAssessment impact) {
RootCauseAnalysis rca = new RootCauseAnalysis();
// Analyze timeline patterns
analyzeTimelinePatterns(timeline, rca);
// Correlate impact with events
correlateImpactWithEvents(impact, timeline, rca);
return rca;
}
private void analyzeTimelinePatterns(Timeline timeline, RootCauseAnalysis rca) {
List<TimelineEvent> events = timeline.getChronologicalEvents();
// Look for deployment-related triggers
boolean hasRecentDeployment = events.stream()
.anyMatch(event -> event.getDescription().toLowerCase().contains("deploy"));
if (hasRecentDeployment) {
rca.getContributingFactors().add("Recent deployment may have introduced instability");
}
// Look for monitoring gaps
long detectionEvents = events.stream()
.filter(event -> "Monitoring".equals(event.getSource()))
.count();
if (detectionEvents == 0) {
rca.getGapsIdentified().add("Incident not detected by monitoring systems");
}
}
private void correlateImpactWithEvents(ImpactAssessment impact, Timeline timeline, 
RootCauseAnalysis rca) {
// Implementation for correlating specific events with impact metrics
}
}

Reporting and Export

1. Postmortem Reporter
@Component
public class PostmortemReporter {
public String generateMarkdownReport(Postmortem postmortem) {
StringBuilder report = new StringBuilder();
report.append("# Postmortem: ").append(postmortem.getTitle()).append("\n\n");
// Executive Summary
report.append("## Executive Summary\n\n");
report.append("**Severity:** ").append(postmortem.getSeverity()).append("\n\n");
report.append("**Duration:** ").append(formatDuration(postmortem.getImpact().getDuration())).append("\n\n");
report.append("**Status:** ").append(postmortem.getStatus()).append("\n\n");
// Incident Summary
report.append("## Incident Summary\n\n");
report.append(postmortem.getSummary().getDescription()).append("\n\n");
// Impact
report.append("## Impact Assessment\n\n");
report.append(generateImpactTable(postmortem.getImpact()));
// Timeline
report.append("## Timeline\n\n");
report.append(generateTimelineTable(postmortem.getTimeline()));
// Root Cause
report.append("## Root Cause Analysis\n\n");
report.append("**Primary Root Cause:** ").append(
postmortem.getRootCauseAnalysis().getPrimaryRootCause()).append("\n\n");
// Action Items
report.append("## Action Items\n\n");
report.append(generateActionItemsTable(postmortem.getActionItems()));
// Lessons Learned
report.append("## Lessons Learned\n\n");
report.append(generateLessonsLearned(postmortem.getLessonsLearned()));
return report.toString();
}
public String generateJsonReport(Postmortem postmortem) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
try {
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(postmortem);
} catch (JsonProcessingException e) {
throw new ReportGenerationException("Failed to generate JSON report", e);
}
}
private String generateActionItemsTable(ActionItems actionItems) {
StringBuilder table = new StringBuilder();
table.append("| ID | Description | Owner | Due Date | Status | Priority |\n");
table.append("|----|-------------|-------|----------|--------|----------|\n");
for (ActionItem item : actionItems.getItems()) {
table.append(String.format("| %s | %s | %s | %s | %s | %s |\n",
item.getId().substring(0, 8),
item.getDescription(),
item.getOwner(),
item.getDueDate() != null ? item.getDueDate().toLocalDate().toString() : "TBD",
item.getStatus(),
item.getPriority()));
}
return table.toString();
}
// Other helper methods for generating report sections...
}
2. Action Item Tracker
@Component
public class ActionItemTracker {
private final ActionItemRepository actionItemRepository;
private final NotificationService notificationService;
@Scheduled(cron = "0 0 9 * * MON") // Every Monday at 9 AM
public void sendWeeklyActionItemReport() {
List<ActionItem> overdueItems = actionItemRepository.findOverdueItems();
List<ActionItem> dueThisWeek = actionItemRepository.findDueThisWeek();
if (!overdueItems.isEmpty() || !dueThisWeek.isEmpty()) {
String report = generateWeeklyReport(overdueItems, dueThisWeek);
notificationService.sendActionItemReport(report);
}
}
public void trackActionItem(ActionItem actionItem) {
actionItemRepository.save(actionItem);
// Schedule reminder for due date
if (actionItem.getDueDate() != null) {
scheduleReminder(actionItem);
}
}
private void scheduleReminder(ActionItem actionItem) {
// Implementation for scheduling reminders
}
public Map<String, Long> getActionItemMetrics() {
return actionItemRepository.findAll().stream()
.collect(Collectors.groupingBy(
item -> item.getOwner(),
Collectors.counting()
));
}
}

REST API Controllers

1. Postmortem Controller
@RestController
@RequestMapping("/api/postmortems")
@Validated
public class PostmortemController {
private final PostmortemManager postmortemManager;
private final PostmortemReporter reporter;
@PostMapping
public ResponseEntity<Postmortem> createPostmortem(@RequestBody @Valid CreatePostmortemRequest request) {
PostmortemBuilder builder = new PostmortemBuilder()
.withBasicInfo(request.getTitle(), request.getSeverity())
.withTiming(request.getStartTime(), request.getEndTime(), 
request.getDetectionTime(), request.getResolutionTime())
.withSummary(request.getDescription(), request.getTrigger(), request.getService())
.withContributors(request.getLeadInvestigator(), request.getContributors());
Postmortem postmortem = postmortemManager.createPostmortem(builder);
return ResponseEntity.status(HttpStatus.CREATED).body(postmortem);
}
@GetMapping("/{id}")
public ResponseEntity<Postmortem> getPostmortem(@PathVariable String id) {
Postmortem postmortem = postmortemManager.findById(id);
return ResponseEntity.ok(postmortem);
}
@GetMapping("/{id}/report")
public ResponseEntity<String> getPostmortemReport(@PathVariable String id, 
@RequestParam(defaultValue = "markdown") String format) {
Postmortem postmortem = postmortemManager.findById(id);
String report;
if ("json".equalsIgnoreCase(format)) {
report = reporter.generateJsonReport(postmortem);
} else {
report = reporter.generateMarkdownReport(postmortem);
}
return ResponseEntity.ok(report);
}
@PostMapping("/{id}/action-items")
public ResponseEntity<Void> addActionItem(@PathVariable String id, 
@RequestBody @Valid ActionItem actionItem) {
postmortemManager.addActionItem(id, actionItem);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@GetMapping("/severity/{severity}")
public ResponseEntity<List<Postmortem>> getPostmortemsBySeverity(@PathVariable Postmortem.IncidentSeverity severity) {
List<Postmortem> postmortems = postmortemManager.findPostmortemsBySeverity(severity);
return ResponseEntity.ok(postmortems);
}
}
// Request DTOs
public class CreatePostmortemRequest {
@NotBlank
private String title;
@NotNull
private Postmortem.IncidentSeverity severity;
@NotNull
private LocalDateTime startTime;
private LocalDateTime endTime;
@NotBlank
private String description;
private String trigger;
private String service;
private String leadInvestigator;
private List<String> contributors;
// getters and setters
}
2. Metrics and Dashboard Controller
@RestController
@RequestMapping("/api/postmortems/metrics")
public class PostmortemMetricsController {
private final PostmortemManager postmortemManager;
private final ActionItemTracker actionItemTracker;
@GetMapping("/summary")
public ResponseEntity<MetricsSummary> getMetricsSummary() {
Map<Postmortem.IncidentSeverity, Long> severityCounts = 
postmortemManager.getPostmortemStatistics();
Map<String, Long> actionItemMetrics = actionItemTracker.getActionItemMetrics();
MetricsSummary summary = new MetricsSummary(severityCounts, actionItemMetrics);
return ResponseEntity.ok(summary);
}
@GetMapping("/trends")
public ResponseEntity<IncidentTrends> getIncidentTrends(
@RequestParam(defaultValue = "30") int days) {
LocalDateTime since = LocalDateTime.now().minusDays(days);
IncidentTrends trends = calculateTrends(since);
return ResponseEntity.ok(trends);
}
}
public class MetricsSummary {
private final Map<Postmortem.IncidentSeverity, Long> incidentsBySeverity;
private final Map<String, Long> actionItemsByOwner;
private final long totalIncidents;
private final long openActionItems;
// constructor, getters
}
public class IncidentTrends {
private final Map<String, Long> incidentsByService;
private final Map<String, Long> incidentsByRootCause;
private final double meanTimeToResolution;
private final double meanTimeToDetection;
// constructor, getters
}

Usage Example

@Service
public class PostmortemService {
private final PostmortemManager postmortemManager;
public void createDatabaseOutagePostmortem() {
Postmortem postmortem = new PostmortemBuilder()
.withBasicInfo("Database Connection Pool Exhaustion", Postmortem.IncidentSeverity.SEV2)
.withTiming(
LocalDateTime.of(2024, 1, 15, 14, 30),
LocalDateTime.of(2024, 1, 15, 16, 45),
LocalDateTime.of(2024, 1, 15, 15, 0),
LocalDateTime.of(2024, 1, 15, 16, 45)
)
.withSummary(
"Database connection pool exhausted due to connection leak in user service",
"Sustained high traffic combined with connection leak",
"user-service"
)
.withImpact(
Duration.ofMinutes(135),
Map.of(
"Revenue Loss", "$2,500",
"Customer Impact", "15% of users affected"
)
)
.withContributors("[email protected]", 
List.of("[email protected]", "[email protected]"))
.build();
// Add timeline events
postmortemManager.addTimelineEvent(postmortem.getId(), 
new TimelineEvent(LocalDateTime.of(2024, 1, 15, 14, 30), 
"High error rates detected in user service", "Monitoring"));
// Add action items
ActionItem actionItem = new ActionItem();
actionItem.setDescription("Implement connection pool monitoring and alerts");
actionItem.setOwner("[email protected]");
actionItem.setDueDate(LocalDateTime.now().plusDays(14));
actionItem.setPriority(ActionItem.Priority.HIGH);
postmortemManager.addActionItem(postmortem.getId(), actionItem);
// Generate and publish report
String report = reporter.generateMarkdownReport(postmortem);
System.out.println(report);
}
}

Best Practices

  1. Blameless Culture: Focus on systemic issues, not individual mistakes
  2. Timely Execution: Conduct postmortems within 1-3 days of incident resolution
  3. Action Item Follow-up: Regularly track and review action item progress
  4. Knowledge Sharing: Make postmortems accessible to the entire organization
  5. Continuous Improvement: Use postmortem insights to improve processes and systems

This Java-based postmortem template provides a structured, automated approach to incident analysis that ensures consistency, enables data-driven insights, and helps organizations learn from failures systematically.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper