Introduction
On-call rotations are a critical component of modern software operations, ensuring that technical teams can respond promptly to incidents and maintain system reliability. Java, with its robust ecosystem and enterprise capabilities, provides an excellent foundation for building sophisticated on-call rotation management systems.
Key Components of an On-Call Rotation System
1. Core Domain Models
public class TeamMember {
private String id;
private String name;
private String email;
private String phoneNumber;
private Set<String> skills;
private boolean active;
// Constructors, getters, and setters
}
public class RotationSchedule {
private String id;
private String teamId;
private RotationType type;
private LocalDateTime startDate;
private LocalDateTime endDate;
private List<RotationShift> shifts;
// Constructors, getters, and setters
}
public enum RotationType {
WEEKLY, BIWEEKLY, MONTHLY, CUSTOM
}
2. Shift Management
public class RotationShift {
private String id;
private String scheduleId;
private String teamMemberId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private ShiftStatus status;
public boolean isActive() {
LocalDateTime now = LocalDateTime.now();
return now.isAfter(startTime) && now.isBefore(endTime);
}
}
public enum ShiftStatus {
SCHEDULED, ACTIVE, COMPLETED, CANCELLED
}
Implementation Strategies
1. Rotation Engine
@Service
public class RotationEngine {
private final TeamMemberRepository teamMemberRepository;
private final RotationScheduleRepository scheduleRepository;
public RotationSchedule createWeeklyRotation(String teamId,
LocalDate startDate,
int weeks) {
List<TeamMember> availableMembers = teamMemberRepository
.findByTeamIdAndActiveTrue(teamId);
RotationSchedule schedule = new RotationSchedule();
schedule.setTeamId(teamId);
schedule.setType(RotationType.WEEKLY);
schedule.setStartDate(startDate.atStartOfDay());
schedule.setEndDate(startDate.plusWeeks(weeks).atStartOfDay());
List<RotationShift> shifts = generateWeeklyShifts(
availableMembers, startDate, weeks);
schedule.setShifts(shifts);
return scheduleRepository.save(schedule);
}
private List<RotationShift> generateWeeklyShifts(
List<TeamMember> members,
LocalDate startDate,
int weeks) {
List<RotationShift> shifts = new ArrayList<>();
int memberCount = members.size();
for (int week = 0; week < weeks; week++) {
for (int day = 0; day < 7; day++) {
TeamMember member = members.get(
(week * 7 + day) % memberCount);
LocalDateTime shiftStart = startDate
.plusWeeks(week)
.plusDays(day)
.atTime(9, 0);
LocalDateTime shiftEnd = shiftStart.plusHours(12);
RotationShift shift = new RotationShift();
shift.setTeamMemberId(member.getId());
shift.setStartTime(shiftStart);
shift.setEndTime(shiftEnd);
shift.setStatus(ShiftStatus.SCHEDULED);
shifts.add(shift);
}
}
return shifts;
}
}
2. Notification Service
@Service
public class NotificationService {
private final EmailService emailService;
private final SmsService smsService;
@Async
public void notifyUpcomingShift(TeamMember member,
RotationShift shift) {
String message = String.format(
"You are scheduled for on-call duty from %s to %s",
shift.getStartTime(), shift.getEndTime());
// Send email notification
emailService.sendNotification(member.getEmail(),
"Upcoming On-Call Shift", message);
// Send SMS notification
smsService.sendSms(member.getPhoneNumber(), message);
}
public void notifyShiftChange(TeamMember oldMember,
TeamMember newMember,
RotationShift shift) {
// Notify both members of the shift change
notifyShiftCancelled(oldMember, shift);
notifyUpcomingShift(newMember, shift);
}
}
3. REST API Controllers
@RestController
@RequestMapping("/api/oncall")
@Validated
public class OnCallController {
private final RotationEngine rotationEngine;
private final OnCallService onCallService;
@PostMapping("/schedules")
public ResponseEntity<RotationSchedule> createSchedule(
@RequestBody @Valid CreateScheduleRequest request) {
RotationSchedule schedule = rotationEngine
.createWeeklyRotation(
request.getTeamId(),
request.getStartDate(),
request.getDurationWeeks());
return ResponseEntity.ok(schedule);
}
@GetMapping("/schedules/{scheduleId}/current")
public ResponseEntity<TeamMember> getCurrentOnCall(
@PathVariable String scheduleId) {
TeamMember currentOnCall = onCallService
.getCurrentOnCallMember(scheduleId);
return ResponseEntity.ok(currentOnCall);
}
@PatchMapping("/shifts/{shiftId}/reassign")
public ResponseEntity<RotationShift> reassignShift(
@PathVariable String shiftId,
@RequestBody @Valid ReassignShiftRequest request) {
RotationShift updatedShift = onCallService
.reassignShift(shiftId, request.getNewMemberId());
return ResponseEntity.ok(updatedShift);
}
}
Advanced Features
1. Escalation Policies
@Entity
public class EscalationPolicy {
private String id;
private String scheduleId;
private List<EscalationLevel> levels;
private int maxEscalationLevel;
public void escalate(Incident incident) {
for (EscalationLevel level : levels) {
if (shouldEscalateToLevel(incident, level)) {
notifyEscalationLevel(level, incident);
}
}
}
}
2. Integration with Monitoring Systems
@Component
public class MonitoringIntegration {
@EventListener
public void handleIncident(IncidentEvent event) {
TeamMember onCallMember = onCallService
.getCurrentOnCallMember(event.getServiceId());
notificationService.notifyIncident(
onCallMember, event.getIncident());
}
}
Best Practices
1. Configuration Management
@Configuration
@ConfigurationProperties(prefix = "oncall")
@Data
public class OnCallProperties {
private int defaultRotationWeeks = 4;
private Duration shiftReminderAdvance = Duration.ofHours(2);
private int maxConsecutiveShifts = 3;
private List<String> mandatorySkills = List.of();
}
2. Error Handling and Resilience
@RestControllerAdvice
public class OnCallExceptionHandler {
@ExceptionHandler(ScheduleConflictException.class)
public ResponseEntity<ErrorResponse> handleConflict(
ScheduleConflictException ex) {
ErrorResponse error = new ErrorResponse(
"SCHEDULE_CONFLICT", ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MemberUnavailableException.class)
public ResponseEntity<ErrorResponse> handleUnavailable(
MemberUnavailableException ex) {
ErrorResponse error = new ErrorResponse(
"MEMBER_UNAVAILABLE", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
Testing Strategy
@SpringBootTest
class OnCallServiceTest {
@Autowired
private OnCallService onCallService;
@Test
void testCreateWeeklyRotation() {
// Given
String teamId = "team-1";
LocalDate startDate = LocalDate.now();
int weeks = 4;
// When
RotationSchedule schedule = onCallService
.createWeeklyRotation(teamId, startDate, weeks);
// Then
assertThat(schedule.getShifts()).hasSize(28); // 4 weeks * 7 days
assertThat(schedule.getType()).isEqualTo(RotationType.WEEKLY);
}
@Test
void testGetCurrentOnCallMember() {
// Given
String scheduleId = "schedule-1";
// When
TeamMember currentOnCall = onCallService
.getCurrentOnCallMember(scheduleId);
// Then
assertThat(currentOnCall).isNotNull();
assertThat(currentOnCall.isActive()).isTrue();
}
}
Conclusion
Building an on-call rotation system with Java provides numerous benefits, including type safety, extensive library support, and robust concurrency handling. By leveraging Spring Boot and other Java ecosystem tools, developers can create reliable, scalable on-call management systems that integrate seamlessly with existing infrastructure and monitoring tools.
The key to success lies in designing flexible domain models, implementing comprehensive notification systems, and ensuring the solution can adapt to various team structures and escalation policies. With proper testing and error handling, Java-based on-call systems can significantly improve incident response times and overall system reliability.