π Reading time: 2 minutes
Have you ever orchestrated services⦠that then fail? If you happen to handle calls to external systems (transcription, storage, AI, etc.), you know that sooner or later something might stop working. But instead of nesting try/catch blocks, you can build something more robust and clean.
In Athanatos, a "little project" of mine I've been working on lately, I used a combination of:
- the orchestrator pattern to coordinate the different services (transcription, OpenAI, storage, etc.)
- the strategy pattern to handle the dynamic fallback in case of an error
π Structure of the flow
- main orchestrator: coordinates the calls to the various services
- specialised sub-orchestrator:
TranscriberService, deals only with transcription. It first tries aPrimaryTranscriberServiceand, in case of an error, delegates the task to aFallbackTranscriberService - fallback strategy: all the transcription strategies (Whisper, AssemblyAI, Google STT) implement the same interface (
ITranscriberService). The implementation changes, but the signature stays identical, allowing the behaviour to vary transparently
The file organisation reflects exactly this logic:
service/
βββ transcriber/
β βββ BaseTranscriberService
β βββ FallbackTranscriberService
β βββ ITranscriberService
β βββ PrimaryTranscriberService
β βββ TranscriberService
βββ OpenAIService
βββ OrchestratorService
βββ StorageService
The heart of the mechanism is really just a few lines: the TranscriberService tries the primary and, if that fails, switches to the fallback thanks to onErrorResume:
@Slf4j
@Service
@RequiredArgsConstructor
public class TranscriberService implements ITranscriberService {
protected final PrimaryTranscriberService primaryTranscriberService;
protected final FallbackTranscriberService fallbackTranscriberService;
public Mono<TranscriptionResponse> transcribe(final PsychometricFileData fileData) {
return primaryTranscriberService.transcribe(fileData)
.doOnError(throwable -> log.error("<-- primary service failed, trying fallback implementation"))
.onErrorResume(throwable -> fallbackTranscriberService.transcribe(fileData));
}
}
π Why does it work?
- decoupling between orchestration and fallback
- clear separation of responsibilities (each service does one thing, in one way only)
- ease of extension: 1 new transcriber = 1 new class
- uniformity of the interface thanks to the strategy pattern
- improved cleanliness and testability
The same approach also adapts well to other real-world contexts, such as multi-provider payments (PayPal, Apple Pay, etc.) or multi-channel notifications (email, SMS, push), where fallback and interchangeable behaviour are essential!
See you at the next pill! β