Code review comment: A countdown latch is a mechanism to block the calling thread until other threads that are running in parallel have completed (counted down). The `.countdown()` essentially decrements a thread-safe counter.  From this article online: https://www.baeldung.com/java-countdown-latch Simply put, a CountDownLatch has a counter field, which you can decrement as we require. We can then use it to block a calling thread until it's been counted down to zero.  If we were doing some parallel processing, we could instantiate the CountDownLatch with the same value for the counter as a number of threads we want to work across. Then, we could just call countdown() after each thread finishes, guaranteeing that a dependent thread calling await() will block until the worker threads are finished.

Code under review:
public static void deleteTwins() throws IOException, InterruptedException {
        System.out.println("DELETE DIGITAL TWINS");
        Map<String, String> twins = FileHelper.loadAllFilesInPath(TwinsPath);

        
        
        for (Map.Entry<String, String> twin : twins.entrySet()) {
            String twinId = twin.getKey();

            List<BasicRelationship> relationshipList = new ArrayList<>();
            Semaphore listRelationshipSemaphore = new Semaphore(0);
            Semaphore deleteRelationshipsSemaphore = new Semaphore(0);
            CountDownLatch deleteTwinsLatch = new CountDownLatch(1);

            
            client.listRelationships(twinId, BasicRelationship.class)
                .doOnNext(relationshipList::add)
                .doOnError(IgnoreNotFoundError)
                .doOnTerminate(listRelationshipSemaphore::release)
                .subscribe();

            
            client.listIncomingRelationships(twinId)
                .doOnNext(e -> relationshipList.add(mapper.convertValue(e, BasicRelationship.class)))
                .doOnError(IgnoreNotFoundError)
                .doOnTerminate(listRelationshipSemaphore::release)
                .subscribe();

            
            if (listRelationshipSemaphore.tryAcquire(2, MaxWaitTimeAsyncOperationsInSeconds, TimeUnit.SECONDS)) {
                relationshipList
                    .forEach(relationship -> client.deleteRelationship(relationship.getSourceId(), relationship.getId())
                        .doOnSuccess(aVoid -> {
                            if (twinId.equals(relationship.getSourceId())) {
                                System.out.println("Found and deleted relationship: " + relationship.getId());
                            } else {
                                System.out.println("Found and deleted incoming relationship: " + relationship.getId());
                            }
                        })
                        .doOnError(IgnoreNotFoundError)
                        .doOnTerminate(deleteRelationshipsSemaphore::release)
                        .subscribe());
            }

            
            if (deleteRelationshipsSemaphore.tryAcquire(relationshipList.size(), MaxWaitTimeAsyncOperationsInSeconds, TimeUnit.SECONDS)) {
                

                
                client.deleteDigitalTwin(twinId)
                    .doOnSuccess(aVoid -> System.out.println("Deleted digital twin: " + twinId))
                    .doOnError(IgnoreNotFoundError)
                    .doOnTerminate(deleteTwinsLatch::countDown)
                    .subscribe();

                
                deleteTwinsLatch.await(MaxWaitTimeAsyncOperationsInSeconds, TimeUnit.SECONDS);
            }
        }
    }


Revised code:
public static void deleteTwins() throws IOException, InterruptedException {
        System.out.println("DELETE DIGITAL TWINS");
        Map<String, String> twins = FileHelper.loadAllFilesInPath(TwinsPath);

        
        
        for (Map.Entry<String, String> twin : twins.entrySet()) {
            String twinId = twin.getKey();

            
            List<BasicRelationship> relationshipList = Collections.synchronizedList(new ArrayList<>());

            
            
            Semaphore listRelationshipSemaphore = new Semaphore(0);
            Semaphore deleteRelationshipsSemaphore = new Semaphore(0);

            
            CountDownLatch deleteTwinsLatch = new CountDownLatch(1);

            
            client.listRelationships(twinId, BasicRelationship.class)
                .doOnNext(relationshipList::add)
                .doOnError(IgnoreNotFoundError)
                .doOnTerminate(listRelationshipSemaphore::release)
                .subscribe();

            
            client.listIncomingRelationships(twinId)
                .doOnNext(e -> relationshipList.add(mapper.convertValue(e, BasicRelationship.class)))
                .doOnError(IgnoreNotFoundError)
                .doOnTerminate(listRelationshipSemaphore::release)
                .subscribe();

            
            if (listRelationshipSemaphore.tryAcquire(2, MaxWaitTimeAsyncOperationsInSeconds, TimeUnit.SECONDS)) {
                relationshipList
                    .forEach(relationship -> client.deleteRelationship(relationship.getSourceId(), relationship.getId())
                        .doOnSuccess(aVoid -> {
                            if (twinId.equals(relationship.getSourceId())) {
                                System.out.println("Found and deleted relationship: " + relationship.getId());
                            } else {
                                System.out.println("Found and deleted incoming relationship: " + relationship.getId());
                            }
                        })
                        .doOnError(IgnoreNotFoundError)
                        .doOnTerminate(deleteRelationshipsSemaphore::release)
                        .subscribe());
            }

            
            if (deleteRelationshipsSemaphore.tryAcquire(relationshipList.size(), MaxWaitTimeAsyncOperationsInSeconds, TimeUnit.SECONDS)) {
                

                
                client.deleteDigitalTwin(twinId)
                    .doOnSuccess(aVoid -> System.out.println("Deleted digital twin: " + twinId))
                    .doOnError(IgnoreNotFoundError)
                    .doOnTerminate(deleteTwinsLatch::countDown)
                    .subscribe();

                
                deleteTwinsLatch.await(MaxWaitTimeAsyncOperationsInSeconds, TimeUnit.SECONDS);
            }
        }
    }