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
- Blameless Culture: Focus on systemic issues, not individual mistakes
- Timely Execution: Conduct postmortems within 1-3 days of incident resolution
- Action Item Follow-up: Regularly track and review action item progress
- Knowledge Sharing: Make postmortems accessible to the entire organization
- 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.