For engineers

Seven services,
one spine.

This site is a working application, not a gallery. Seven independent .NET microservices behind one Nginx gateway, sharing no database and no contracts library — they cooperate through domain events on a RabbitMQ bus. Here's exactly how it's put together.

Every service exists to solve one problem

Not a demo grid. Each one owns a real problem end to end — its own database, its own rules, its own failure modes. Here is the problem each was built for, and the part that was genuinely hard.

01portfolio-identity

Proving who you are, without renting it out

Every other service needs to trust a claim about who is calling it. The easy path is to buy that from a provider and never learn how it works. I built it instead — the moment you hand off identity, you stop understanding your own security model.

C#.NET 8JWTTOTP 2FA

What it solves

  • Registration with real email confirmation, so accounts are reachable
  • JWT sessions every other service can verify without calling back
  • TOTP two-factor with recovery codes, for people who lose their phone
  • Password reset that cannot be used to enumerate valid accounts

The hard part

Token lifetime. Long-lived tokens are convenient and unrevocable; short-lived ones need a refresh path that survives two browser tabs racing each other. That race is the whole problem, and it is in no tutorial.

02portfolio-household

Four roommates, one fridge, no record of who did what

Shared living runs on memory, and memory is partisan. Everyone remembers taking out the trash; nobody remembers whose turn it is. The argument is never really about chores — it is about the absence of a shared record.

EF CorePostgresRabbitMQ

What it solves

  • Recurring chores with an assignee, so the same person is not always on dishes
  • A shared calendar every member sees the same version of
  • Member roles — an owner can invite and remove, a member cannot
  • Ownership transfer, because the person who set it up moves out

The hard part

Membership is the truth every other service depends on. Someone who leaves mid-month still owes for the days they lived there — so "removed" can never mean "deleted". Departures are recorded, never erased.

03portfolio-finance

Splitting a bill fairly is an accounting problem

Everyone starts with a spreadsheet and a payment app, and it works until rent is late, someone moves in mid-month, or a bill splits three ways instead of four. Then nobody trusts the number and the spreadsheet gets abandoned.

C#EF CorePostgresOutbox

What it solves

  • Charges split by who was actually a member on the date of the charge
  • Double-entry accruals, so every balance traces back to its source
  • Settlements recorded as their own event — paying is not deleting
  • Income tracking, so a household can see whether it covers its costs

The hard part

Money demands exactness under retries. A network blip must not post a charge twice, so every consumer checks an idempotency table before acting, and the ledger write shares one transaction with the event that announced it.

04portfolio-forum

A conversation that stays worth reading

Any comment box works on day one. It fails on day sixty, when one account dominates every thread and there is no way to remove them without also removing the discussion they were part of.

C#RabbitMQSSE

What it solves

  • Threads and nested comments with vote-weighted ordering
  • Live vote counts pushed over SSE — no polling, no refresh
  • Moderators appointed per community, not globally
  • Bans that hide future posts without erasing the thread

The hard part

Votes are the write-heavy path in the whole system. Recomputing a total on every vote does not hold up, so votes are recorded as facts and the total is derived — the number on screen and the number in the database converge instead of being locked together.

05portfolio-notifications

Telling people something happened, exactly once

The naive version has every service notify its own users. Then one feature ships, four services each announce the same event, and a person gets four messages about one thing.

C#RabbitMQSSE

What it solves

  • One consumer for every domain event in the system
  • Persisted history, so a notification survives a closed tab
  • Server-sent events push to open clients with no polling
  • Read state tracked per person, per notification

The hard part

This service reaches into nobody — it only listens. That constraint means a new feature elsewhere gets notifications for free, and it is the clearest proof the event bus is doing real work.

Two utilities, open to everyone

No account, no stored data. They make a point: not every service deserves a database or a place on the message bus.

06No login

Weather, without the ad wall

Search any city, flip °F and °C, see it on a map. The interesting part is what it does not do — no account, no database, no bus. A stateless service that can be redeployed or thrown away at any moment.

.NET 8OpenWeather
07No login

Unit conversion you can trust

Length, mass, temperature, volume, speed, area, data. The conversions live in a domain model rather than as a pile of magic numbers in a controller — the same discipline as the services that handle money.

.NET 8DDD

And the two that hold it together

08

One interface over seven backends

This app. Seven services with seven different shapes have to feel like one product — one nav, one auth state, one loading vocabulary. React Query owns the cache, so acting in one module never leaves another showing stale data.

TypeScriptNext.js 15React Query
09

The whole system, one command

Nine repositories are worthless if standing them up takes a weekend. This one pulls prebuilt images, wires every service to its database and the bus, and puts Nginx in front — so the environment is reproducible instead of remembered.

DockerComposeNginx

Architecture

Why seven, not one?

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
Each service is drawn around what's likely to change together — not around a noun. They never touch each other's tables. The bus is the only crossing.
Design philosophy · IDesign × DDD

Bounded contexts, one bus

Who owns what · what they publish

Five 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 only

Geography

Stateless weather lookups

off-bus

Math

Stateless unit conversion

off-bus

One event, end to end

Finance.Domain.Events.ChargeCreated

A 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.

  1. 01 · request thread

    The tap

    A member submits an expense from the household ledger UI.

  2. 02 · request thread

    Command handler

    MediatR routes it to the Finance handler, which validates against the domain rules.

  3. 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.

  4. 04 · response

    The user is already done

    HTTP returns here. Everything below happens on a background thread.

  5. 05 · background

    Outbox dispatch

    The dispatcher picks the row up and publishes ChargeCreated to RabbitMQ via MassTransit.

  6. 06 · background

    Consumers, exactly once

    Each consumer checks an idempotency table before acting, so a redelivery can't double-post.

  7. 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.

The stack

Services

.NET 8ASP.NET CoreMediatREF CorePostgres 16

Messaging

RabbitMQMassTransitTransactional outboxIdempotency table

Frontend

Next.js 15TypeScriptReact QueryTailwindRadix

Edge & infra

Nginx gatewayDocker ComposeAWSGitHub Actions

Skills

Languages & frameworks

C#.NETASP.NETTypeScriptReactNext.jsNode.jsPythonGroovy

Architecture

MicroservicesDomain-Driven DesignRESTful APIsEvent-driven (RabbitMQ)Legacy modernizationSystem design

Cloud & DevOps

AWSAzureDockerCI/CDGitContainerization

Data

OraclePL/SQLSQLMongoDBDynamoDBMariaDBData pipelines

Testing & QA

Automated testingSeleniumAPI testingTestRailTest design

Leadership & agile

Certified ScrumMasterSprint planningRetrospectivesStakeholder collaboration