Every static call graph is built from syntax. A tool parses files, sees service.persist(order), and draws an edge from the calling function to persist. That works exactly as long as the compiler or parser can decide which persist is meant 1. A visual call graph produced from profiler data shows real calls, but only for the run it profiled; the map is a snapshot, not a guarantee 2.
Dynamic dispatch breaks the assumption. The implementation is chosen at run time based on the runtime type of the receiver, so the source shows a call to a name, not to a function 3. Polymorphism is the point: a File object and a Database object both expose StoreRecord, and the caller does not know or care which one it holds 4. A graph that picks one or draws edges to all implementations is guessing.
This is not a small edge case. A study that ran 13 static analysis tools plus one dynamic analysis over 1,000 Android apps found that, on average, the static tools failed to capture 61% of the methods that actually executed at run time. Apps built around external frameworks were the worst offenders, and the authors note that higher precision in call graph construction goes hand in hand with more unsoundness 5. Callback-driven and event-driven frameworks invoke application methods implicitly; the call lives in the framework, which is often not analyzed along with the app 6.
Your web service has the same shape. The framework calls your controller. The dependency injection container constructs your class and calls its constructor. The plugin loader calls handle. None of those call sites appear in your repository, because they sit in a library.



