Edit: Second attempt at this answer as I realized I was getting carried away in microscopic details. I'll try to simplify here.
Observer Pattern/Event Queue
So among the most straightforward solutions to this problem is to use something like Observer or some kind of centralized event queue. Your collision detection system can then either be called immediately in an indirect way as an observer of the subject (your game scene) or it can be handed a set of events that occurred indirectly/abstractly from the central system and then discover what happened to the scene (ex: element being inserted/removed). Then you can respond by updating your spatial index accordingly.
However, I have grown a strong distaste for these types of solutions over the years in large projects because, in the unfortunate case that you have to debug something that goes wrong during an event/observer notification, I find that can turn into a nightmarish scenario where I feel like I'm being bounced around like I'm in a pinball machine all over the codebase trying to figure out what went wrong.
Of course I still use these solutions here and there, but try not to lean on them too much. In particular I try to avoid like a recursive scenario where one event emits one or more additional events, or where one observer gets notified only to do something which causes another subject (or worse, same one) to then notify more observers.
"Diffs"
So the main alternative I prefer nowadays is to, instead of either of these solutions, use some kind of data structure which allows you to find like a "changeset" or "delta/diff" of sorts in hindsight.
The data structure is fairly lightweight to copy around and keeps track of the info needed so that you can compare a new version of itself with an old version like new_version.diff(old_version)
and it'll tell you everything you need to know as to what happened between the time you stored the old version of it and the time you received the new one, like what elements have been inserted/removed to/from the scene. That's a bit vague as to how you design this data structure but it can vary wildly based on your needs, but the general idea is like this and you can tackle it in many ways.
And I actually started using this solution as a practical requirement. In my case I no longer have one central instance of a scene. I have all sorts of threads/systems running in parallel all storing their own local copy of a scene and generating new modified versions which then get output to some other systems (ex: renderer) which then store their own copy and run in parallel and so forth. So there is no central scene instance in my case anymore which makes any kind of central Observer/event queue impractical if not outright impossible, and this sort of "diffs" solution seemed the most practical way to solve the problem. And in my case that "diffs" kind of functionality is part of the scene itself (new_scene.diff(old_scene)
) which then tells me relevant info like what entities have been inserted/removed from the previous copy of the scene I had, and the scene itself an immutable persistent data structure.
Yet after doing that, I've found it so much easier since there's no longer a need to have all this communication between objects/systems/threads to tell each other, directly or indirectly, what has happened to the previous state. Your objects/systems/threads can just look at the new state (game scene) and discover, at their own leisure, in their own processing thread, what happened and make the necessary updates to whatever structures in response (ex: spatial index).
Now typically what I'm recommending is not a list of events that occurred, though that might seem the most straightforward way to keep track of what has happened between new version and old. It's not very practical to compare an entire growing list of things that happened to each other to figure out changes that way, and even if you mitigate the infinite desire for growth by storing some additional data like a "cache point", that's starting to get a bit unwieldy still. What I do in my case is start with a bitset data structure which keeps track of what entities occupy what indices. Comparing old bitset to new using rapid set operations then immediately tells you what entities no longer occupy former indices and what entities now occupy new ones not formerly occupied. But of course that doesn't tell you if some entity was removed and a new one was inserted to occupy its former index, for example, so I also use some change counters and things like that on top of the set operations to quickly drill down and discover what elements have been removed/inserted/modified. That's a basic example and my real-world ones get a lot more nuanced, but should give you a start as to one way you might implement this.