diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonMentionPropagationListener.java b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonMentionPropagationListener.java
new file mode 100644
index 00000000..0009a162
--- /dev/null
+++ b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonMentionPropagationListener.java
@@ -0,0 +1,69 @@
+package org.raddatz.familienarchiv.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent;
+import org.raddatz.familienarchiv.model.PersonMention;
+import org.raddatz.familienarchiv.model.TranscriptionBlock;
+import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * Transcription-domain consumer of {@link PersonDisplayNameChangedEvent}. When
+ * Person.getDisplayName() flips during a rename, this listener rewrites every
+ * transcription block whose sidecar references the renamed person — both the
+ * literal "@OldName" inside block.text and the displayName carried in the
+ * {@link PersonMention} entries.
+ *
+ *
Synchronous on purpose: the rename and the propagation must commit as one
+ * transaction so a half-applied rewrite never reaches the archive. If the
+ * archive grows past tens of thousands of blocks, switch to
+ * {@code @TransactionalEventListener(AFTER_COMMIT) + @Async} — one annotation
+ * change.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class PersonMentionPropagationListener {
+
+ private final TranscriptionBlockRepository blockRepository;
+ private final PersonService personService;
+
+ @EventListener
+ @Transactional
+ public void onPersonDisplayNameChanged(PersonDisplayNameChangedEvent event) {
+ if (!personService.existsById(event.personId())) {
+ log.warn("Skipping mention propagation for non-existent personId {}", event.personId());
+ return;
+ }
+
+ List blocks =
+ blockRepository.findByMentionedPersons_PersonId(event.personId());
+ if (blocks.isEmpty()) {
+ return;
+ }
+
+ String oldNeedle = "@" + event.oldDisplayName();
+ String newNeedle = "@" + event.newDisplayName();
+
+ for (TranscriptionBlock block : blocks) {
+ if (block.getText() != null) {
+ block.setText(block.getText().replace(oldNeedle, newNeedle));
+ }
+ for (PersonMention mention : block.getMentionedPersons()) {
+ if (mention.getPersonId().equals(event.personId())) {
+ mention.setDisplayName(event.newDisplayName());
+ }
+ }
+ }
+
+ blockRepository.saveAll(blocks);
+
+ log.info("Propagated rename {} → {} across {} block(s) for person {}",
+ event.oldDisplayName(), event.newDisplayName(), blocks.size(), event.personId());
+ }
+}
diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java
index c83050ac..191c8530 100644
--- a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java
+++ b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java
@@ -50,6 +50,10 @@ public class PersonService {
.orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id));
}
+ public boolean existsById(UUID id) {
+ return personRepository.existsById(id);
+ }
+
public List findCorrespondents(UUID personId, String q) {
if (q != null && !q.isBlank()) {
return personRepository.findCorrespondentsWithFilter(personId, q);
diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/PersonMentionPropagationListenerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/PersonMentionPropagationListenerTest.java
new file mode 100644
index 00000000..a07de40e
--- /dev/null
+++ b/backend/src/test/java/org/raddatz/familienarchiv/service/PersonMentionPropagationListenerTest.java
@@ -0,0 +1,98 @@
+package org.raddatz.familienarchiv.service;
+
+import jakarta.persistence.EntityManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.raddatz.familienarchiv.PostgresContainerConfig;
+import org.raddatz.familienarchiv.config.FlywayConfig;
+import org.raddatz.familienarchiv.model.Document;
+import org.raddatz.familienarchiv.model.DocumentAnnotation;
+import org.raddatz.familienarchiv.model.DocumentStatus;
+import org.raddatz.familienarchiv.model.Person;
+import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent;
+import org.raddatz.familienarchiv.model.PersonMention;
+import org.raddatz.familienarchiv.model.TranscriptionBlock;
+import org.raddatz.familienarchiv.repository.AnnotationRepository;
+import org.raddatz.familienarchiv.repository.DocumentRepository;
+import org.raddatz.familienarchiv.repository.PersonRepository;
+import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
+import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Import({PostgresContainerConfig.class, FlywayConfig.class})
+class PersonMentionPropagationListenerTest {
+
+ @Autowired TranscriptionBlockRepository blockRepository;
+ @Autowired DocumentRepository documentRepository;
+ @Autowired AnnotationRepository annotationRepository;
+ @Autowired PersonRepository personRepository;
+ @Autowired EntityManager em;
+
+ private PersonMentionPropagationListener listener;
+ private PersonService personService;
+
+ private UUID documentId;
+ private UUID annotationId;
+
+ @BeforeEach
+ void setUp() {
+ personService = mock(PersonService.class);
+ when(personService.existsById(any())).thenReturn(true);
+ listener = new PersonMentionPropagationListener(blockRepository, personService);
+
+ Document doc = documentRepository.save(Document.builder()
+ .title("Letter").originalFilename("letter.pdf")
+ .status(DocumentStatus.UPLOADED).build());
+ documentId = doc.getId();
+ DocumentAnnotation annotation = annotationRepository.save(DocumentAnnotation.builder()
+ .documentId(documentId).pageNumber(1)
+ .x(0.1).y(0.2).width(0.3).height(0.4)
+ .color("#00C7B1").build());
+ annotationId = annotation.getId();
+ }
+
+ private TranscriptionBlock saveBlock(String text, List mentions) {
+ return blockRepository.saveAndFlush(TranscriptionBlock.builder()
+ .annotationId(annotationId).documentId(documentId)
+ .text(text).sortOrder(0)
+ .mentionedPersons(mentions).build());
+ }
+
+ private UUID savedPersonId(String firstName, String lastName) {
+ Person p = personRepository.save(Person.builder()
+ .firstName(firstName).lastName(lastName).build());
+ return p.getId();
+ }
+
+ @Test
+ void rewritesTextAndSidecar_whenSingleBlockReferencesRenamedPerson() {
+ UUID personId = savedPersonId("Auguste", "Raddatz");
+ TranscriptionBlock saved = saveBlock(
+ "Liebe Tante @Auguste Raddatz, danke für den Brief.",
+ List.of(new PersonMention(personId, "Auguste Raddatz")));
+ em.clear();
+
+ listener.onPersonDisplayNameChanged(
+ new PersonDisplayNameChangedEvent(personId, "Auguste Raddatz", "Augusta Raddatz"));
+ blockRepository.flush();
+ em.clear();
+
+ TranscriptionBlock reloaded = blockRepository.findById(saved.getId()).orElseThrow();
+ assertThat(reloaded.getText()).isEqualTo("Liebe Tante @Augusta Raddatz, danke für den Brief.");
+ assertThat(reloaded.getMentionedPersons())
+ .extracting(PersonMention::getDisplayName)
+ .containsExactly("Augusta Raddatz");
+ }
+}