Skip to content

Refactor monolithic IErrorMessageDataStore into specialized interfaces - #5661

Open
johnsimons wants to merge 1 commit into
masterfrom
john/split_large_interface
Open

Refactor monolithic IErrorMessageDataStore into specialized interfaces#5661
johnsimons wants to merge 1 commit into
masterfrom
john/split_large_interface

Conversation

@johnsimons

Copy link
Copy Markdown
Member

Split IErrorMessageDataStore

IErrorMessageDataStore had grown to 32 members referenced from 41 files, spanning message views, failed-message queries, status transitions, retry support, failure groups, notifications, message editing, failed-import writes, and event-log writes. This splits it along those seams and deletes it.

Two things drove the grouping:

  • Consumers already cluster cleanly. No consumer touched more than one of the new interfaces, apart from ArchiveMessageHandler, LegacyMessageFailureResolvedHandler, MessageFailureResolvedHandler, and EditFailedMessagesController, which each touch exactly two and now take two dependencies.
  • The EFCore stub already annotated one cluster: exactly five members carried a // must set StatusChangedAt + LastModified comment, and those are precisely the status-mutating writes. That shared invariant is now one interface.

New interfaces

Interface Members Consumers
IMessagesViewDataStore 5 the scatter-gather APIs in CompositeViews/Messages
IFailedMessageQueryDataStore 7 GetAllErrorsController, GetErrorByIdController, EditFailedMessagesController, ArchiveMessageHandler, MessageFailedPublisher
IFailedMessageLifecycleDataStore 5 the archive/unarchive/resolved handlers, ReturnToSenderDequeuer
IFailedMessageRetryDataStore 4 PendingRetriesHandler, RetriesHandler, FailedMessageRetryCleaner, ReturnToSender, MessageFailureResolvedHandler
INotificationsDataStore 1 NotificationsController, SendEmailNotificationHandler
IEditFailedMessagesDataStore 1 EditFailedMessagesController, EditHandler

IFailedMessageRetryDataStore is deliberately separate from IRetryDocumentDataStore, which is about retry batches.

Absorbed into existing interfaces

  • IGroupsDataStore gains 7 members: GetGroup, GetGroupErrors, GetGroupErrorsCount, GetFailureGroupView, EditComment, DeleteComment, and GetArchivedFailureGroupsByClassifier. That last one was called GetFailureGroupsByClassifier, which collided with the method of the same name already on IGroupsDataStore. They are different queries (ArchivedGroupsViewIndex vs FailureGroupsViewIndex), so the incoming one is renamed.
  • IFailedErrorImportDataStore gains StoreFailedErrorImport. It already owned the read side (ProcessFailedErrorImports, QueryContainsFailedImports); only the write half was orphaned.
  • StoreEventLogItem folds into IEventLogDataStore.Add. See below.

Renames

Before After
ErrorGet GetFailedMessages
ErrorsHead GetFailedMessagesStats
ErrorsByEndpointName GetFailedMessagesByEndpoint
ErrorsSummary GetFailedMessagesSummary
ErrorLastBy GetLatestFailedMessageView
ErrorBy GetFailedMessage
FailedMessagesFetch GetFailedMessagesByIds
FailedMessageMarkAsArchived MarkAsArchived
MarkMessageAsResolved MarkAsResolved
RemoveFailedMessageRetryDocument RemoveFailedMessageRetry
FetchFromFailedMessage GetFailedMessageBody

"Document" was RavenDB vocabulary leaking into a provider-agnostic abstraction, and FetchFromFailedMessage returns the body bytes. HTTP action names and routes are untouched, so there is no API surface change.

One behavioural decision worth reviewing

StoreEventLogItem and IEventLogDataStore.Add were near-duplicates in RavenDB: the former called expirationManager.EnableExpiration(session, logItem), the latter did not. Add had no production caller, and the only writer of event-log items (AuditEventLogWriter) went through StoreEventLogItem, so expiration was in effect for every event-log item actually written. The consolidated Add keeps the expiration call, preserving current behaviour rather than silently dropping it. Flagging it explicitly because it is the one change here that is not purely structural.

Implementation shape

Interface segregation did not force class segregation:

  • RavenDB keeps its single ErrorMessagesDataStore implementing four of the new interfaces, wired with forwarding registrations, matching the pattern already used for ExternalIntegrationRequestsDataStore. The group members moved into Recoverability/GroupsDataStore, the import writer plus its MakeDocumentId/CollectionName statics into FailedErrorImportDataStore, and the two factory methods into small new stores under Editing/.
  • EFCore splits into one class per interface, so the 27 members still unimplemented there can be picked up in parallel without contention on a single file. StoreFailedErrorImport was already implemented and moves across as-is.

Tests

The two full-interface test doubles shrink from 32 members to 16 and 8. PersistenceTestBase now exposes one accessor per interface instead of two overlapping IErrorMessageDataStore properties, and gains a shared FailedImportStore (the local copy in FailedErrorImportTests is removed).

This change decomposes the single, large `IErrorMessageDataStore` interface into several smaller, more focused interfaces such as `IMessagesViewDataStore`, `IFailedMessageQueryDataStore`, `IFailedMessageLifecycleDataStore`, `IFailedMessageRetryDataStore`, `IEditFailedMessagesDataStore`, and `INotificationsDataStore`.

This refactoring improves the separation of concerns, enhances testability, and aligns the codebase with the Interface Segregation and Single Responsibility Principles. Existing implementations and consumers across both EFCore and RavenDB persistence layers, as well as application logic, have been updated to utilize these new, more specific contracts.
@johnsimons johnsimons self-assigned this Jul 29, 2026
@johnsimons
johnsimons marked this pull request as ready for review July 29, 2026 02:57
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