A portfolio app could be a single .NET project. This one isn't, for a specific reason: I wanted the seams to be visible. Each service is drawn around a volatility — a part of the system likely to change for its own reasons — rather than around a noun like "User" or a verb like "Manage". That's Juval Löwy's IDesign rule of thumb, and it's the only decomposition I've found that survives feature growth.
Finance owns the rules of money. Household owns the people who share it. Forum owns public speech. Identity owns who you are. None of them reach into another's database. The only crossing is a domain event on the bus — and consumers bind to the publisher's real event type, not a sanitized integration contract. Less indirection, less drift.
At a glance
- Services
- Five on the bus, two off
- Bus
- RabbitMQ · MassTransit
- Reliability
- Outbox + idempotency
- Edge
- Nginx · Docker · AWS
Bounded contexts, one bus
Who owns what · what they publishFive services participate in the messaging story. Geography and Math are pure utility services and live off-bus — they neither publish nor consume events.
Identity
Owns who you are
UserRegisteredUserProfileUpdatedUserBanned
Household
Owns the people who share it
MemberJoinedMemberLeftOwnershipTransferred
Finance
Owns the rules of money
ChargeCreatedAllocationCreatedSettlementRecorded
Forum
Owns public speech
ThreadCreatedCommentCreatedModeratorAppointed
RabbitMQ spine
MassTransit · transactional outbox · idempotent consumers
Notifications
Consumes every event, fans out to clients over SSE
consumer onlyGeography
Stateless weather lookups
off-busMath
Stateless unit conversion
off-busOne event, end to end
Finance.Domain.Events.ChargeCreatedA single tap on "Add expense" travels through seven stages. The user's HTTP request returns before the bus even hears about it — the publish is decoupled by the transactional outbox.
01 · request thread
The tap
A member submits an expense from the household ledger UI.
02 · request thread
Command handler
MediatR routes it to the Finance handler, which validates against the domain rules.
03 · request thread
One transaction, two writes
The Charge aggregate and an outbox row — the serialized event — commit in the same Postgres transaction. If either fails, both roll back and the bus never sees a phantom event.
04 · response
The user is already done
HTTP returns here. Everything below happens on a background thread.
05 · background
Outbox dispatch
The dispatcher picks the row up and publishes ChargeCreated to RabbitMQ via MassTransit.
06 · background
Consumers, exactly once
Each consumer checks an idempotency table before acting, so a redelivery can't double-post.
07 · background
The books get written
Finance takes its own event back off the bus and posts the double-entry accrual into the group's ledger. A failed posting redelivers instead of being lost.