Hi Martin,
Sure, there is a solution to notify screens from business logic and persistence layers. It’s based on Spring’s application events and works within a single server instance.
Firstly, let’s consider a simple solution with the UiEventPublisher bean provided by the framework. It delivers events to screens opened by the current user in a web browser tab. In other words, if a user clicks some button in a browser tab, and this button click eventually invokes the code that sends an event, the event will be delivered to screens opened in that tab.
Create an event class:
package com.company.app.entity;
import org.springframework.context.ApplicationEvent;
public class FooChangedEvent extends ApplicationEvent {
    public FooChangedEvent(Object source) {
        super(source);
    }
}
Create an event listener in the screen which should be notified. In the example below it’s the main screen:
public class MainScreen extends Screen implements Window.HasWorkArea {
// ...
    @Autowired
    private Notifications notifications;
    @EventListener
    private void fooChanged(FooChangedEvent event) {
        notifications.create().withCaption("A Foo instance has been changed").show();
    }
Send the event using the UiEventPublisher:
@Component
public class FooEventListener {
    @Autowired
    private UiEventPublisher uiEventPublisher;
    @TransactionalEventListener
    public void onFooChangedAfterCommit(EntityChangedEvent<Foo> event) {
        uiEventPublisher.publishEvent(new FooChangedEvent(this));
    }
}
If you want to deliver an event to all currently connected users and to all their browser tabs, you need to create a more complex publisher in your project. Below is a working example - it’s a bean that registers all Vaadin sessions created on the server and iterates through them when sending events:
package com.company.app;
import com.vaadin.server.VaadinSession;
import io.jmix.ui.App;
import io.jmix.ui.AppUI;
import io.jmix.ui.event.AppInitializedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Component
public class GlobalUiEventPublisher {
    private static final Logger log = LoggerFactory.getLogger(GlobalUiEventPublisher.class);
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final List<WeakReference<VaadinSession>> sessions = new ArrayList<>();
    @EventListener
    public void onAppStart(AppInitializedEvent event) {
        lock.writeLock().lock();
        try {
            sessions.add(new WeakReference<>(VaadinSession.getCurrent()));
        } finally {
            lock.writeLock().unlock();
        }
    }
    public void publishEvent(ApplicationEvent event) {
        ArrayList<VaadinSession> activeSessions = new ArrayList<>();
        int removed = 0;
        lock.readLock().lock();
        try {
            for (Iterator<WeakReference<VaadinSession>> iterator = sessions.iterator(); iterator.hasNext(); ) {
                WeakReference<VaadinSession> reference = iterator.next();
                VaadinSession session = reference.get();
                if (session != null) {
                    activeSessions.add(session);
                } else {
                    lock.readLock().unlock();
                    lock.writeLock().lock();
                    try {
                        iterator.remove();
                        lock.readLock().lock();
                    } finally {
                        lock.writeLock().unlock();
                    }
                    removed++;
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        if (removed > 0) {
            log.debug("Removed {} Vaadin sessions", removed);
        }
        log.debug("Sending {} to {} Vaadin sessions", event, activeSessions.size());
        for (VaadinSession session : activeSessions) {
            // obtain lock on session state
            session.access(() -> {
                if (session.getState() == VaadinSession.State.OPEN) {
                    // active app in this session
                    App app = App.getInstance();
                    // notify all opened web browser tabs
                    List<AppUI> appUIs = app.getAppUIs();
                    for (AppUI ui : appUIs) {
                        if (!ui.isClosing()) {
                            // work in context of UI
                            ui.accessSynchronously(() -> {
                                ui.getUiEventsMulticaster().multicastEvent(event);
                            });
                        }
                    }
                }
            });
        }
    }
}
Use this publisher instead of the built-in one:
@Component
public class FooEventListener {
    @Autowired
    private GlobalUiEventPublisher globalUiEventPublisher;
    @TransactionalEventListener
    public void onFooChangedAfterCommit(EntityChangedEvent<Foo> event) {
        globalUiEventPublisher.publishEvent(new FooChangedEvent(this));
    }
}
Regards,
Konstantin