πŸ• 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:

πŸ“Œ Structure of the flow

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?

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! β˜•