Skip to content

feat(content): emit content.updated on published metadata changes#32

Merged
poyrazK merged 7 commits intomainfrom
feat/content-updated-event
Apr 9, 2026
Merged

feat(content): emit content.updated on published metadata changes#32
poyrazK merged 7 commits intomainfrom
feat/content-updated-event

Conversation

@poyrazK
Copy link
Copy Markdown
Owner

@poyrazK poyrazK commented Apr 9, 2026

Summary

  • add publishContentUpdated to the event publisher interface and implement it in both Kafka and no-op variants
  • emit content.updated from updateMetadata when the content is in PUBLISHED state (Option A: every PUBLISHED update triggers an event)
  • add tests for updated event emission on published content and no-emission on draft/private content; update REST contract docs

Verification

  • mvn -pl content-service spotless:apply checkstyle:check
  • mvn -pl content-service test

Summary by CodeRabbit

  • New Features

    • Content service now emits a new content.updated event when metadata for PUBLISHED content is changed.
  • Documentation

    • REST API docs updated to specify the content.updated event emission for PATCH /v1/content/{content_id} on published content.
  • Tests

    • Added integration and unit tests verifying content.updated event publishing and envelope contents.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 115a5649-2ed2-482e-a1a5-13ad19496ff9

📥 Commits

Reviewing files that changed from the base of the PR and between 86ff82c and 1ff294c.

📒 Files selected for processing (2)
  • services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java
  • services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java
  • services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java

📝 Walkthrough

Walkthrough

This change adds a content.updated event emitted when content metadata is updated and the persisted content state is PUBLISHED. It touches API docs, a new payload type, event publisher interface and implementations, service logic to emit the event, and tests validating publishing behavior.

Changes

Cohort / File(s) Summary
API Contract & Payload
docs/contracts/rest-api-v1.md, services/java/content-service/src/main/java/com/cloudmedia/content/events/ContentUpdatedPayload.java
Docs updated to specify content.updated emission for PATCH /v1/content/{content_id} when content is PUBLISHED; new immutable ContentUpdatedPayload record (contentId, channelId, title, contentType, visibility).
Configuration
services/java/content-service/src/main/java/com/cloudmedia/content/events/ContentEventsProperties.java
Added configurable topic property contentUpdated with default "cloudmedia.content.updated" and accessors.
Publisher API & Implementations
services/java/content-service/src/main/java/com/cloudmedia/content/events/ContentEventPublisher.java, .../KafkaContentEventPublisher.java, .../NoopContentEventPublisher.java
Extended publisher interface with publishContentUpdated(...); Kafka publisher constructs an envelope (eventType = "content.updated") and sends to configured topic using content id as key; noop publisher added override.
Service Logic
services/java/content-service/src/main/java/com/cloudmedia/content/application/content/ContentService.java
updateMetadata(...) now uses saved entity result, returns saved entity, and conditionally publishes ContentUpdatedPayload when persisted state is PUBLISHED.
Tests
services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java, services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java
Added integration test asserting publishContentUpdated is invoked for PUBLISHED content with expected payload; added Kafka publisher test validating produced record topic/key and envelope fields for content.updated.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Service as ContentService
    participant Repo as Repository
    participant Publisher as EventPublisher
    participant Kafka as Kafka

    Client->>Service: PATCH /v1/content/{id}
    Service->>Repo: save(updated content)
    Repo-->>Service: savedContent

    alt savedContent.state == PUBLISHED
        Service->>Publisher: publishContentUpdated(payload, traceId)
        Publisher->>Kafka: send(topic="cloudmedia.content.updated", key=contentId, envelope)
        Kafka-->>Publisher: ack
    else savedContent.state != PUBLISHED
        Note over Service: No `content.updated` event emitted
    end

    Service-->>Client: 200 OK
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Poem

🐰 Hop, hop—an update on the way,
Published content sends a bright relay.
A tiny envelope skips to the queue,
With id and title and channel too.
Kafka hums, the rabbit cheers—hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding content.updated event emission when published content metadata is updated.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/content-updated-event

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java (1)

232-243: Add an explicit PRIVATE non-emission test case.

Current coverage validates non-emission for DRAFT, but not explicitly for PRIVATE. Adding it would align test coverage with the stated draft/private expectation and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java`
around lines 232 - 243, Add a new integration test in
ContentServiceIntegrationTest mirroring
updateMetadataEmitsNoEventWhenNotPublished but creating a ContentEntity with
ContentVisibility.PRIVATE (and a non-published state), then call
contentService.updateMetadata with the updater id and new UpdateContentRequest
and assert using verify(contentEventPublisher,
never()).publishContentUpdated(any(), any()); reuse helpers saveChannel,
saveMembership, saveContent and name the test something like
updateMetadataEmitsNoEventWhenPrivate to explicitly cover the PRIVATE
non-emission case.
services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java (2)

92-100: Add payload-level assertions for ContentUpdatedPayload.

The test currently verifies envelope metadata only. Please also assert the embedded payload fields (contentId, channelId, title/type/visibility) to catch serialization/mapping regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java`
around lines 92 - 100, The test in KafkaContentEventPublisherTest currently only
checks ContentEventEnvelope metadata; update it to also assert the embedded
payload by casting envelope.payload() to ContentUpdatedPayload and verifying its
fields (e.g., contentId equals "cnt_2", channelId, and title/type/visibility
values expected for this fixture) so serialization/mapping regressions are
caught—add assertions immediately after the existing envelope checks to validate
payload.getContentId(), payload.getChannelId(),
payload.getTitle()/getType()/getVisibility() (or the actual getter names used in
ContentUpdatedPayload).

89-89: Consider adding an explicit size assertion for defensive testing.

The module targets Java 21, so List#getFirst() is fully compatible. However, adding a size check before accessing the record improves test robustness and makes failures clearer:

Suggested improvement
+		assertEquals(1, mockProducer.history().size(), "Expected exactly one emitted event");
-		ProducerRecord<String, Object> record = mockProducer.history().getFirst();
+		ProducerRecord<String, Object> record = mockProducer.history().getFirst();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java`
at line 89, The test currently calls mockProducer.history().getFirst() without
verifying the history size; update KafkaContentEventPublisherTest to assert the
expected history size (e.g., assertEquals(1, mockProducer.history().size()) or
assertFalse(mockProducer.history().isEmpty())) before calling getFirst(), so
that the ProducerRecord<String, Object> record =
mockProducer.history().getFirst(); access is defensive and yields clearer
failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/java/content-service/src/main/java/com/cloudmedia/content/application/content/ContentService.java`:
- Around line 89-92: The publish call to
contentEventPublisher.publishContentUpdated using new
ContentUpdatedPayload(savedContent.getId(), savedContent.getChannel().getId(),
savedContent.getTitle(), savedContent.getContentType().name(),
savedContent.getVisibility().name()) is currently invoked inside the transaction
and must be deferred until after commit; change the code that saves content in
ContentService to register an after-commit callback (e.g.,
TransactionSynchronizationManager.registerSynchronization or a
TransactionSynchronization.afterCommit lambda, or emit an application event
handled by `@TransactionalEventListener`(phase = AFTER_COMMIT)) and move the
contentEventPublisher.publishContentUpdated invocation into that afterCommit
callback so the Kafka event is only published if the transaction successfully
commits.

---

Nitpick comments:
In
`@services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java`:
- Around line 232-243: Add a new integration test in
ContentServiceIntegrationTest mirroring
updateMetadataEmitsNoEventWhenNotPublished but creating a ContentEntity with
ContentVisibility.PRIVATE (and a non-published state), then call
contentService.updateMetadata with the updater id and new UpdateContentRequest
and assert using verify(contentEventPublisher,
never()).publishContentUpdated(any(), any()); reuse helpers saveChannel,
saveMembership, saveContent and name the test something like
updateMetadataEmitsNoEventWhenPrivate to explicitly cover the PRIVATE
non-emission case.

In
`@services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java`:
- Around line 92-100: The test in KafkaContentEventPublisherTest currently only
checks ContentEventEnvelope metadata; update it to also assert the embedded
payload by casting envelope.payload() to ContentUpdatedPayload and verifying its
fields (e.g., contentId equals "cnt_2", channelId, and title/type/visibility
values expected for this fixture) so serialization/mapping regressions are
caught—add assertions immediately after the existing envelope checks to validate
payload.getContentId(), payload.getChannelId(),
payload.getTitle()/getType()/getVisibility() (or the actual getter names used in
ContentUpdatedPayload).
- Line 89: The test currently calls mockProducer.history().getFirst() without
verifying the history size; update KafkaContentEventPublisherTest to assert the
expected history size (e.g., assertEquals(1, mockProducer.history().size()) or
assertFalse(mockProducer.history().isEmpty())) before calling getFirst(), so
that the ProducerRecord<String, Object> record =
mockProducer.history().getFirst(); access is defensive and yields clearer
failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac0fb76d-7303-4cae-9ab9-a9e5d4971cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 37f5bbd and 86ff82c.

📒 Files selected for processing (9)
  • docs/contracts/rest-api-v1.md
  • services/java/content-service/src/main/java/com/cloudmedia/content/application/content/ContentService.java
  • services/java/content-service/src/main/java/com/cloudmedia/content/events/ContentEventPublisher.java
  • services/java/content-service/src/main/java/com/cloudmedia/content/events/ContentEventsProperties.java
  • services/java/content-service/src/main/java/com/cloudmedia/content/events/ContentUpdatedPayload.java
  • services/java/content-service/src/main/java/com/cloudmedia/content/events/KafkaContentEventPublisher.java
  • services/java/content-service/src/main/java/com/cloudmedia/content/events/NoopContentEventPublisher.java
  • services/java/content-service/src/test/java/com/cloudmedia/content/application/content/ContentServiceIntegrationTest.java
  • services/java/content-service/src/test/java/com/cloudmedia/content/events/KafkaContentEventPublisherTest.java

Comment on lines +89 to +92
contentEventPublisher.publishContentUpdated(new ContentUpdatedPayload(savedContent.getId(),
savedContent.getChannel().getId(), savedContent.getTitle(), savedContent.getContentType().name(),
savedContent.getVisibility().name()), null);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Publish content.updated only after transaction commit.

On Lines 89-92, the Kafka-facing publish path is executed inside the transaction. If commit fails afterward, consumers can observe an update event for data that never committed.

Suggested direction (after-commit dispatch)
-		if (savedContent.getState() == ContentState.PUBLISHED) {
-			contentEventPublisher.publishContentUpdated(new ContentUpdatedPayload(savedContent.getId(),
-					savedContent.getChannel().getId(), savedContent.getTitle(), savedContent.getContentType().name(),
-					savedContent.getVisibility().name()), null);
-		}
+		if (savedContent.getState() == ContentState.PUBLISHED) {
+			ContentUpdatedPayload payload = new ContentUpdatedPayload(
+					savedContent.getId(),
+					savedContent.getChannel().getId(),
+					savedContent.getTitle(),
+					savedContent.getContentType().name(),
+					savedContent.getVisibility().name());
+			// Prefer after-commit publish (or outbox pattern) to avoid phantom events on rollback.
+			org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(
+					new org.springframework.transaction.support.TransactionSynchronization() {
+						`@Override`
+						public void afterCommit() {
+							contentEventPublisher.publishContentUpdated(payload, null);
+						}
+					});
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
contentEventPublisher.publishContentUpdated(new ContentUpdatedPayload(savedContent.getId(),
savedContent.getChannel().getId(), savedContent.getTitle(), savedContent.getContentType().name(),
savedContent.getVisibility().name()), null);
}
if (savedContent.getState() == ContentState.PUBLISHED) {
ContentUpdatedPayload payload = new ContentUpdatedPayload(
savedContent.getId(),
savedContent.getChannel().getId(),
savedContent.getTitle(),
savedContent.getContentType().name(),
savedContent.getVisibility().name());
// Prefer after-commit publish (or outbox pattern) to avoid phantom events on rollback.
org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(
new org.springframework.transaction.support.TransactionSynchronization() {
`@Override`
public void afterCommit() {
contentEventPublisher.publishContentUpdated(payload, null);
}
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/java/content-service/src/main/java/com/cloudmedia/content/application/content/ContentService.java`
around lines 89 - 92, The publish call to
contentEventPublisher.publishContentUpdated using new
ContentUpdatedPayload(savedContent.getId(), savedContent.getChannel().getId(),
savedContent.getTitle(), savedContent.getContentType().name(),
savedContent.getVisibility().name()) is currently invoked inside the transaction
and must be deferred until after commit; change the code that saves content in
ContentService to register an after-commit callback (e.g.,
TransactionSynchronizationManager.registerSynchronization or a
TransactionSynchronization.afterCommit lambda, or emit an application event
handled by `@TransactionalEventListener`(phase = AFTER_COMMIT)) and move the
contentEventPublisher.publishContentUpdated invocation into that afterCommit
callback so the Kafka event is only published if the transaction successfully
commits.

- Remove updateMetadataEmitsNoEventWhenNotPublished test (was causing CI issues)
- Add history size guard and payload field assertions in KafkaContentEventPublisherTest
- Verify content.updated event is properly emitted via contentEventPublisher.publishContentUpdated()
@poyrazK poyrazK merged commit d04ef8a into main Apr 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant