| name | translation-slice |
| description | Generate translators that map external events (from other bounded contexts or systems) into internal domain events using Wolverine handlers |
TRANSLATION Slice Skill
Overview
A TRANSLATION slice bridges an external event (from another bounded context, a legacy system, or a third-party integration) into the internal domain language. It is an Anti-Corruption Layer (ACL) — it translates foreign concepts into your domain's events without coupling the core domain to external contracts.
How it differs from other slice types
| Pattern | Trigger | Output | Purpose |
|---|
| STATE_CHANGE | User command | Domain event | Change aggregate state |
| AUTOMATION | Internal event | Internal command | Orchestrate within domain |
| TRANSLATION | External event/message | Internal domain event | Bridge external → internal |
| STATE_VIEW | Internal event | Read model | Query side |
Flow Pattern
External System / Other Context
│ publishes external message/event
▼
Wolverine Handler (Translator)
│ maps external → internal domain event
▼
Marten Event Stream (appends internal event)
│
▼
Domain projections, automations, read models react normally
Java → C# / Critter Stack Mapping
| Java / Axon | C# / Critter Stack |
|---|
@EventHandler on external event type | Wolverine Handle(ExternalEvent, IDocumentSession) |
AggregateLifecycle.apply() inside handler | session.Events.Append(id, internalEvent) |
@ProcessingGroup for integration events | Wolverine transport subscription (RabbitMQ, ASB, etc.) or in-process |
| External DTO record | Plain C# record in Integrations/<Source>/ |
Code Generation Steps
Step 1: Define the External Message Type
Location: Integrations/<SourceSystem>/<ExternalEventName>.cs
namespace SlicingWorkshop.Integrations.<SourceSystem>;
public record <ExternalEventName>(
string ExternalId,
// fields as received from external system
);
Step 2: Create Internal Domain Event (if not already existing)
Location: Events/<InternalEventName>.cs
using SlicingWorkshop.Common;
namespace SlicingWorkshop.Events;
public record <InternalEventName>(
Guid AggregateId,
// fields in domain language
) : IEvent;
Step 3: Create Translator Handler
Location: <Context>/Slices/<SliceName>/Internal/<SliceName>Translator.cs
using Marten;
using SlicingWorkshop.Events;
using SlicingWorkshop.Integrations.<SourceSystem>;
namespace SlicingWorkshop.<Context>.Slices.<SliceName>.Internal;
public static class <SliceName>Translator
{
public static async Task Handle(
<ExternalEventName> external,
IDocumentSession session)
{
var internalEvent = new <InternalEventName>(
AggregateId: Guid.Parse(external.ExternalId),
);
session.Events.Append(internalEvent.AggregateId, internalEvent);
await session.SaveChangesAsync();
}
}
When the aggregate ID must be derived or looked up:
public static async Task Handle(
<ExternalEventName> external,
IDocumentSession session,
IQuerySession querySession)
{
var readModel = await querySession
.Query<<SomeReadModel>>()
.FirstOrDefaultAsync(x => x.ExternalRef == external.ExternalId);
if (readModel is null) return;
var internalEvent = new <InternalEventName>(readModel.Id, );
session.Events.Append(readModel.Id, internalEvent);
await session.SaveChangesAsync();
}
Step 4: Wire up the External Message Source
For in-process (same app, different context):
No extra configuration — Wolverine discovers the handler by convention.
For external transport (RabbitMQ, Azure Service Bus, etc.) — add to Program.cs:
builder.Host.UseWolverine(opts =>
{
opts.UseRabbitMq(rabbit => rabbit.HostName = "localhost")
.AutoProvision()
.BindExchange("<external-exchange>")
.ToQueue("<queue-name>");
opts.ListenToRabbitMqQueue("<queue-name>");
});
Step 5: Create Tests
Location: SlicingWorkshop.Tests/<Context>/Slices/<SliceName>/<SliceName>TranslatorTests.cs
using Marten;
using SlicingWorkshop.Events;
using SlicingWorkshop.Integrations.<SourceSystem>;
using Xunit;
namespace SlicingWorkshop.Tests.<Context>.Slices.<SliceName>;
public class <SliceName>TranslatorTests : IClassFixture<AppFixture>
{
private readonly AppFixture _fixture;
public <SliceName>TranslatorTests(AppFixture fixture) => _fixture = fixture;
[Fact]
public async Task TranslatesExternalEventToInternalEvent()
{
var external = new <ExternalEventName>(
ExternalId: Guid.NewGuid().ToString()
);
using var session = _fixture.Store.LightweightSession();
await <SliceName>Translator.Handle(external, session);
var aggregateId = Guid.Parse(external.ExternalId);
var events = await session.Events.FetchStreamAsync(aggregateId);
Assert.Contains(events, e => e.Data is <InternalEventName>);
}
}
Key Principles
- Never let external types leak into domain code — the translator is the only place that knows about the external contract
- Translate terminology, not just fields — rename concepts to match the ubiquitous language (e.g.,
userId → CustomerId, orderRef → OrderId)
- Idempotency — translators may receive duplicates; check if the event was already appended when needed
- No business logic — the translator maps; business rules stay in the aggregate
- External types live in
Integrations/<SourceSystem>/ — isolated from domain and events folders