profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

main default branch 501 files Expires Sep 13, 2026, 9:06 AM
README.md 15,281 bytes

SplitApp — Trip Expense Management (Microservices)

SplitApp is an ASP.NET Core 10.0 web application for managing group trips and splitting expenses. Users create trips, invite friends, track costs with flexible splitting (equal / equal-subset / exact / percentage), manage budgets, run polls, maintain a wishlist, and settle debts via an optimized algorithm.

This repo is the microservices implementation. It builds on the modular-monolith version (splitapp-backend-modular-monolith) by extracting the Users module into a separate microservice (SplitApp.UsersService) with its own database. The two processes communicate via RabbitMQ (inter-service RPC + events) and HTTP REST (login / register / admin CRUD). Runs locally via docker compose (webapp + users-service + RabbitMQ + two Postgres databases).


Run

docker compose -p splitapp-phase4 up --build -d

Brings up five containers:

Service Container Host port Notes
webapp phase4-webapp http://localhost:97 The MVC monolith (Trips + Expenses + Admin UX)
users-service phase4-users-service http://localhost:98 The Users microservice — Identity, JWT issuance, admin REST API
rabbitmq phase4-rabbitmq http://localhost:15672 (UI), :5672 Message broker (guest / guest)
db-monolith phase4-db-monolith (internal) PostgreSQL 16, splitapp database (Trips + Expenses)
db-users phase4-db-users (internal) PostgreSQL 16, splitapp_users database (Identity + RefreshTokens)

Module migrations run automatically on each service's startup. The users-service seeds the 6 test users; the webapp skips example-trip seeding (the Users service doesn't expose seed data without admin auth — see "Known limitations" below).

Test login (after seed):

  • user@example.com, alice@example.com, bob@example.com, charlie@example.com, diana@example.com / Kala.12345 — regular users. The password is in the source on purpose: this is a demo and the data is invented.
  • The admin account is seeded only when SEED_ADMIN_PASSWORD is set, and there is no default. Without the variable there is no administrator at all.

Stop:

docker compose -p splitapp-phase4 down       # keep data volumes
docker compose -p splitapp-phase4 down -v    # also drop the named volumes

Architecture at a glance

┌──────────────────────┐    HTTP REST (login/register/admin)   ┌──────────────────────┐
│  SplitApp.WebApp     │ ◄──────────────────────────────────►  │  SplitApp.UsersService│
│  (monolith, port 97) │                                        │  (microservice, 98)   │
│                      │   RabbitMQ (RPC + events, JSON)        │                       │
│  - Trips module      │ ◄──────────────────────────────────►  │  - Users.Domain       │
│  - Expenses module   │                                        │  - Users.Application  │
│  - MVC client UX     │                                        │  - Users.Infrastructure│
│  - Full Admin UX     │                                        │  - Users.Api (REST)   │
│  - JWT (validate)    │                                        │  - Identity + UoW     │
│  - No Users.* refs   │                                        │  - JWT issuer         │
└──────────┬───────────┘                                        └───────────┬───────────┘
           │                                                                │
           ▼                                                                ▼
   ┌───────────────┐                                                ┌───────────────┐
   │ db-monolith   │              ┌──────────────┐                  │ db-users      │
   │ (Postgres 16) │              │   rabbitmq   │                  │ (Postgres 16) │
   │ trips + exp.  │              │ exchanges:   │                  │ Identity +    │
   │               │              │  splitapp.   │                  │ refresh tokens│
   │               │              │   events     │                  │               │
   │               │              │  splitapp.   │                  │               │
   │               │              │   requests   │                  │               │
   └───────────────┘              └──────────────┘                  └───────────────┘

Communication contract:

Concern Channel Direction
Login / Register / Refresh / Logout HTTP /api/v1/identity/account/* WebApp → UsersService
Admin user list / edit / delete / roles HTTP /api/v1/identity/admin/* WebApp → UsersService
Inter-module user lookup (id → DisplayName/Email) RabbitMQ RPC (GetUserByIdRequest, GetUsersByIdsRequest) WebApp → UsersService
User deletion fan-out RabbitMQ event (UserDeletedEvent) UsersService → WebApp
JWT validation Local (shared HS256 key in config) both services validate identically

Reference rules:

  • SplitApp.WebApp has zero project references to any SplitApp.Modules.Users.* project.
  • Cross-process inter-module function calls go through RabbitMQ only (via the IUserLookup abstraction in SplitApp.Shared.Messaging).
  • Trips.Api + Expenses.Api controllers + AppUnitOfWork use IUserLookup (RabbitMQ RPC), not in-process MediatR, for user data.
  • Inside the monolith (Trips ↔ Expenses), MediatR is still used in-process for cross-module calls — only Users moved out of process.

See architecture.md for the full architecture deep-dive.


Solution layout

SplitApp.Modular/
├── SplitApp.sln
├── Directory.Build.props
├── src/
│   ├── SplitApp.WebApp/                                  ← monolith host (Trips + Expenses + MVC + Admin)
│   │   ├── Program.cs                                    ← JWT bearer + cookie reader, AddMessaging, AddHttpClient<IUsersServiceClient>
│   │   ├── Application/
│   │   │   ├── Services/  (+ Admin/)                      ← lifted phase-2 BLL — IUsersServiceClient for user lookups
│   │   │   ├── DTO/                                       ← BllDtos (no AppUser refs anywhere)
│   │   │   ├── Mappers/                                   ← Domain ↔ BllDto factories (UserDto-based)
│   │   │   ├── Persistence/AppUnitOfWork.cs               ← Trips + Expenses DbContexts, IUserLookup for user hydration
│   │   │   ├── Persistence/CrossModuleNavigationLoader.cs ← uses IUserLookup (RabbitMQ RPC)
│   │   │   ├── UsersService/                              ← typed HttpClient + JwtForwardingHandler
│   │   │   └── Messaging/UserDeletedEventHandler.cs       ← RabbitMQ event subscriber
│   │   ├── Controllers/AccountController.cs               ← MVC login/register/logout (replaces Areas/Identity)
│   │   ├── Areas/Admin/                                   ← full admin UX, UsersController calls HTTP REST
│   │   └── Resources/                                     ← i18n .resx (EN + ET)
│   ├── Services/Users/SplitApp.UsersService/              ← NEW microservice host
│   │   ├── Program.cs                                     ← AddUsersModule + MassMessaging consumers + Swagger
│   │   ├── Messaging/                                     ← GetUserByIdRequestHandler, GetUsersByIdsRequestHandler
│   │   ├── Controllers/HealthController.cs                ← /health endpoint (gated on seed completion)
│   │   └── Hosting/                                       ← Swagger config + SeededHealthState
│   ├── Shared/
│   │   ├── SplitApp.Shared.Kernel/                        ← BaseEntity, IBaseRepo, IUoW, LangStr, IdentityHelpers
│   │   ├── SplitApp.Shared.Contracts/                     ← in-process MediatR contracts (Trips ↔ Expenses)
│   │   └── SplitApp.Shared.Messaging/                     ← RabbitMQ wrapper, IUserLookup, integration messages
│   └── Modules/
│       ├── Users/                                          ← consumed by SplitApp.UsersService (NOT by WebApp)
│       ├── Trips/                                          ← in-process module in WebApp
│       └── Expenses/                                       ← in-process module in WebApp
└── tests/
    ├── SplitApp.Modules.Users.Tests/
    ├── SplitApp.Modules.Trips.Tests/
    ├── SplitApp.Modules.Expenses.Tests/
    ├── SplitApp.Shared.Messaging.Tests/                    ← NEW: integration message contract tests
    └── SplitApp.WebApp.IntegrationTests/                   ← architecture invariants + HTTP smoke

The Modules/Users/* projects still exist in the modular monolith layout — but they are consumed only by the new SplitApp.UsersService host. The SplitApp.WebApp.csproj has zero <ProjectReference> entries pointing at any Users.* project.


URL map

Webapp (port 97)

URL Purpose
/ Landing page (MVC)
/Account/{Login, Register, Logout, Manage} MVC auth (HTTP → Users service → JWT cookie)
/Trips, /Trips/{Create,Details/{id},Edit/{id},Delete/{id}} Trip CRUD
/Members?tripId={id} and /Members/AcceptInvitation/{token} Trip participants + invitation flow
/Expenses?tripId={id} (with Create/Edit/Delete) Trip expenses
/Budget?tripId={id} (CreateCategory/EditCategory/DeleteCategory) Budget categories
/Settlement?tripId={id} Balances + settlement plans
/PollsClient?tripId={id} (Create/Details) Trip polls
/WishlistClient?tripId={id} Trip wishlist
/Admin/Dashboard Admin home (admin role)
/Admin/Users Admin user list — proxies to users-service via HTTP
/Admin/{Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist} Admin CRUD per entity
/swagger Swagger UI — Trips + Expenses REST only

Users service (port 98)

URL Purpose
/swagger Swagger UI for all Users REST endpoints
/health Healthcheck (200 only after migrate + seed completes)
/api/v1/identity/account/{register, login, logout, refreshtokendata} Auth (anonymous)
/api/v1/identity/admin/users (GET/PUT/DELETE/+ /{id}/roles, /admin/roles) Admin user CRUD ([Authorize(Roles = "admin")])

RabbitMQ management (port 15672)

  • Login guest / guest
  • Exchanges: splitapp.events (topic), splitapp.requests (direct)
  • Queues: q.webapp.UserDeletedEvent, q.users-service.GetUserByIdRequest, q.users-service.GetUsersByIdsRequest

Inter-service messaging

All cross-service communication goes through the SplitApp.Shared.Messaging library. Two patterns:

Pattern Used for Library API
Pub/Sub (events) Fire-and-forget fan-out (UserDeletedEvent) IMessageBus.PublishEventAsync<T> + IEventHandler<T>
RPC (requests) Synchronous data lookups (GetUserByIdRequest, GetUsersByIdsRequest) IMessageBus.RequestAsync<TReq, TResp> + IRequestHandler<TReq, TResp>

For the WebApp, IUserLookup is the high-level facade over the bus — controllers and services consume IUserLookup, not IMessageBus directly.

In-process MediatR is still used between Trips ↔ Expenses modules (they live in the same process). Only the Users module crossed the process boundary.


Tests

cd SplitApp.Modular
dotnet test

53 tests across five projects, all passing:

Project Tests Covers
SplitApp.Modules.Users.Tests 8 IdentityHelpers — JWT generation/validation round-trips
SplitApp.Modules.Trips.Tests 12 LangStr — multi-language fallback, edge cases
SplitApp.Modules.Expenses.Tests 10 CurrencyConverter — exchange rates, rounding
SplitApp.Shared.Messaging.Tests 9 Wire-format contract round-trips (GetUserByIdRequest, UserDeletedEvent, UserDto, …) + marker interface invariants + queue naming
SplitApp.WebApp.IntegrationTests 14 Architecture invariants (module boundaries, schema isolation) + WebApplicationFactory<Program> HTTP smoke

Phase 4 ↔ Phase 3 mapping

Phase 3 (modular monolith) Phase 4 (microservices)
1 process (SplitApp.WebApp) 2 processes (SplitApp.WebApp + SplitApp.UsersService)
1 Postgres database, 3 schemas (users/trips/expenses) 2 Postgres databases (splitapp + splitapp_users)
In-process MediatR for all cross-module calls RabbitMQ (Users.*) + in-process MediatR (Trips/Expenses)
UsersDbContext registered in WebApp host UsersDbContext registered in SplitApp.UsersService only
Areas/Identity scaffolded Razor Pages handle login/register New Controllers/AccountController.cs posts to Users service via HTTP, stores JWT in HttpOnly cookie
Admin/UsersController uses UserManager<AppUser> directly Admin/UsersController uses typed IUsersServiceClient HTTP client
[NotMapped] AppUser? CreatedBy on Trip/Expense entities [NotMapped] UserDto? CreatedBy — entities no longer reference Users domain types
~13 WebApp files inject UserManager/SignInManager/RoleManager All replaced with User.FindFirstValue(ClaimTypes.NameIdentifier) claim reads
44 tests 53 tests (added Shared.Messaging.Tests)
One container deploy (docker compose -p splitapp-phase3 up) Five-container compose with healthcheck-gated startup

Known limitations

  • No example trip data on first boot. AppDataInit.SeedExampleData calls IUsersServiceClient.ListUsersAsync() without a JWT (it runs from startup, not an HTTP request context). The Users service returns 401, the try/catch swallows it, and example trips don't get seeded. Core user accounts (admin, alice, …) ARE seeded by the Users service itself, so login works — you just create your own trips from scratch.
  • Graceful degradation if RabbitMQ is unavailable. IUserLookup catches MessageBusTimeout/MessageBusUnavailableException and returns empty results, so pages render with blank user names rather than crashing. Logs show the warning.

Files & docs