Composer has a specific way of telling you a package is a bad long-term bet: when a dependency is marked abandoned on Packagist, the lock file itself records "abandoned": true against that package's entry, and every install or update that touches it prints a warning. During the Drupal 10 to 11 upgrade on this site, doctrine/annotations is exactly that package — flagged abandoned, and also one Drupal 11 currently can't do without.
How it disappeared, then had to come back
doctrine/annotations was already in the project before the upgrade, at version 1.14.4, pulled in transitively by something else in the dependency tree. Moving drupal/core-recommended to ^11 needed a full -W resolve to succeed at all, and somewhere in that resolution, doctrine/annotations dropped out of the lock file completely. Nothing in the new tree required it anymore, at least not by Composer's read of the declared dependency graph.
Except Drupal 11 does still need it, just not through a normal require line. Core ships AttributeDiscoveryWithAnnotations, a compatibility shim that lets contrib modules still using the old @Annotation-style plugin definitions keep working under Drupal 11's PHP-attribute-based plugin discovery, and that shim depends on doctrine/annotations at runtime. Composer's solver has no way to see that dependency — it only knows what's declared on Packagist, not what Drupal core actually calls internally. So a package still genuinely load-bearing had already fallen out of the lock file, and the failure mode wouldn't have shown up as a composer error. It would have shown up as some contrib module's annotation-based plugin quietly breaking on a production request, discovered by watching something throw a class-not-found rather than by reading a dependency-resolution log.
What the abandoned flag is actually telling you
Re-adding it explicitly as doctrine/annotations: ^2.0 in composer.json is where that flag actually shows up: the version Composer resolves to, 2.0.2, carries "abandoned": true directly in composer.lock. That flag doesn't mean the package is broken, or unsafe to run today. It means the Doctrine project has stopped maintaining it and isn't planning to start again. Those are different claims. "Nobody's maintaining this" is a reason to plan an eventual replacement; it is not, on its own, a reason to assume the current dependency graph doesn't need the package right now. Drupal core building its own annotation-compatibility layer against an abandoned Doctrine package isn't a decision available to a downstream site to override — pulling the dependency back out because Packagist says to doesn't remove the need for it, it just breaks the path core is relying on to fill that need.
The dependency it dragged back in
Re-adding doctrine/annotations at ^2.0 wasn't free either. That version requires doctrine/lexer at ^2 || ^3, and resolving that against everything else already in the tree moved the project's doctrine/lexer from 2.1.1 to 3.0.1 — a package I hadn't referenced anywhere directly and had no reason to be thinking about, moving because of a dependency two hops away from anything Drupal-specific. Nothing about that particular bump caused a problem here. It's the same shape of surprise as the original drop, though: a decision about one named package, doctrine/annotations, had a traceable consequence on a second package, doctrine/lexer, that never appeared on either side of a composer require command.
What changes if you take the warning literally
The instinct an "abandoned" warning is designed to trigger — stop depending on this, find something else — is correct advice in general and the wrong response applied mechanically here. The warning describes the package's own maintenance status, not whether your dependency graph, or something built on top of it, still calls into it. Drupal 11 core needing an unmaintained Doctrine package to keep older contrib annotations working isn't something a site maintainer gets to opt out of; it's built into core's own bridge for a transition — annotations giving way to PHP attributes — that hasn't finished across the whole contrib ecosystem yet. The right response to the warning here wasn't removing the dependency. It was checking whether anything real still called into it before deciding what to do, which in this case meant re-adding it on purpose rather than leaving its presence to chance during the next full dependency resolve.
That's the actual habit worth keeping from this: an abandoned flag is Packagist describing a maintainer's intentions, not a live read of your own application's call graph. Whether something is still load-bearing is a separate question, and Composer's solver — even doing a full -W re-resolve — isn't positioned to answer that question when the real caller is application code sitting a layer above anything Composer can see.