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
1 # SplitApp — Trip Expense Management (Microservices)
2
3 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.
4
5 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).
6
7 ---
8
9 ## Run
10
11 ```bash
12 docker compose -p splitapp-phase4 up --build -d
13 ```
14
15 Brings up five containers:
16
17 | Service | Container | Host port | Notes |
18 |---|---|---|---|
19 | `webapp` | `phase4-webapp` | http://localhost:97 | The MVC monolith (Trips + Expenses + Admin UX) |
20 | `users-service` | `phase4-users-service` | http://localhost:98 | The Users microservice — Identity, JWT issuance, admin REST API |
21 | `rabbitmq` | `phase4-rabbitmq` | http://localhost:15672 (UI), :5672 | Message broker (guest / guest) |
22 | `db-monolith` | `phase4-db-monolith` | (internal) | PostgreSQL 16, `splitapp` database (Trips + Expenses) |
23 | `db-users` | `phase4-db-users` | (internal) | PostgreSQL 16, `splitapp_users` database (Identity + RefreshTokens) |
24
25 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).
26
27 Test login (after seed):
28 - `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.
29 - 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.
30
31 Stop:
32
33 ```bash
34 docker compose -p splitapp-phase4 down # keep data volumes
35 docker compose -p splitapp-phase4 down -v # also drop the named volumes
36 ```
37
38 ---
39
40 ## Architecture at a glance
41
42 ```
43 ┌──────────────────────┐ HTTP REST (login/register/admin) ┌──────────────────────┐
44 │ SplitApp.WebApp │ ◄──────────────────────────────────► │ SplitApp.UsersService│
45 │ (monolith, port 97) │ │ (microservice, 98) │
46 │ │ RabbitMQ (RPC + events, JSON) │ │
47 │ - Trips module │ ◄──────────────────────────────────► │ - Users.Domain │
48 │ - Expenses module │ │ - Users.Application │
49 │ - MVC client UX │ │ - Users.Infrastructure│
50 │ - Full Admin UX │ │ - Users.Api (REST) │
51 │ - JWT (validate) │ │ - Identity + UoW │
52 │ - No Users.* refs │ │ - JWT issuer │
53 └──────────┬───────────┘ └───────────┬───────────┘
54 │ │
55 ▼ ▼
56 ┌───────────────┐ ┌───────────────┐
57 │ db-monolith │ ┌──────────────┐ │ db-users │
58 │ (Postgres 16) │ │ rabbitmq │ │ (Postgres 16) │
59 │ trips + exp. │ │ exchanges: │ │ Identity + │
60 │ │ │ splitapp. │ │ refresh tokens│
61 │ │ │ events │ │ │
62 │ │ │ splitapp. │ │ │
63 │ │ │ requests │ │ │
64 └───────────────┘ └──────────────┘ └───────────────┘
65 ```
66
67 **Communication contract:**
68
69 | Concern | Channel | Direction |
70 |---|---|---|
71 | Login / Register / Refresh / Logout | HTTP `/api/v1/identity/account/*` | WebApp → UsersService |
72 | Admin user list / edit / delete / roles | HTTP `/api/v1/identity/admin/*` | WebApp → UsersService |
73 | Inter-module user lookup (id → DisplayName/Email) | RabbitMQ RPC (`GetUserByIdRequest`, `GetUsersByIdsRequest`) | WebApp → UsersService |
74 | User deletion fan-out | RabbitMQ event (`UserDeletedEvent`) | UsersService → WebApp |
75 | JWT validation | Local (shared HS256 key in config) | both services validate identically |
76
77 **Reference rules:**
78 - `SplitApp.WebApp` has **zero project references** to any `SplitApp.Modules.Users.*` project.
79 - Cross-process inter-module function calls go through **RabbitMQ only** (via the `IUserLookup` abstraction in `SplitApp.Shared.Messaging`).
80 - `Trips.Api` + `Expenses.Api` controllers + `AppUnitOfWork` use `IUserLookup` (RabbitMQ RPC), not in-process MediatR, for user data.
81 - Inside the monolith (Trips ↔ Expenses), MediatR is still used in-process for cross-module calls — only **Users** moved out of process.
82
83 See [architecture.md](architecture.md) for the full architecture deep-dive.
84
85 ---
86
87 ## Solution layout
88
89 ```
90 SplitApp.Modular/
91 ├── SplitApp.sln
92 ├── Directory.Build.props
93 ├── src/
94 │ ├── SplitApp.WebApp/ ← monolith host (Trips + Expenses + MVC + Admin)
95 │ │ ├── Program.cs ← JWT bearer + cookie reader, AddMessaging, AddHttpClient<IUsersServiceClient>
96 │ │ ├── Application/
97 │ │ │ ├── Services/ (+ Admin/) ← lifted phase-2 BLL — IUsersServiceClient for user lookups
98 │ │ │ ├── DTO/ ← BllDtos (no AppUser refs anywhere)
99 │ │ │ ├── Mappers/ ← Domain ↔ BllDto factories (UserDto-based)
100 │ │ │ ├── Persistence/AppUnitOfWork.cs ← Trips + Expenses DbContexts, IUserLookup for user hydration
101 │ │ │ ├── Persistence/CrossModuleNavigationLoader.cs ← uses IUserLookup (RabbitMQ RPC)
102 │ │ │ ├── UsersService/ ← typed HttpClient + JwtForwardingHandler
103 │ │ │ └── Messaging/UserDeletedEventHandler.cs ← RabbitMQ event subscriber
104 │ │ ├── Controllers/AccountController.cs ← MVC login/register/logout (replaces Areas/Identity)
105 │ │ ├── Areas/Admin/ ← full admin UX, UsersController calls HTTP REST
106 │ │ └── Resources/ ← i18n .resx (EN + ET)
107 │ ├── Services/Users/SplitApp.UsersService/ ← NEW microservice host
108 │ │ ├── Program.cs ← AddUsersModule + MassMessaging consumers + Swagger
109 │ │ ├── Messaging/ ← GetUserByIdRequestHandler, GetUsersByIdsRequestHandler
110 │ │ ├── Controllers/HealthController.cs ← /health endpoint (gated on seed completion)
111 │ │ └── Hosting/ ← Swagger config + SeededHealthState
112 │ ├── Shared/
113 │ │ ├── SplitApp.Shared.Kernel/ ← BaseEntity, IBaseRepo, IUoW, LangStr, IdentityHelpers
114 │ │ ├── SplitApp.Shared.Contracts/ ← in-process MediatR contracts (Trips ↔ Expenses)
115 │ │ └── SplitApp.Shared.Messaging/ ← RabbitMQ wrapper, IUserLookup, integration messages
116 │ └── Modules/
117 │ ├── Users/ ← consumed by SplitApp.UsersService (NOT by WebApp)
118 │ ├── Trips/ ← in-process module in WebApp
119 │ └── Expenses/ ← in-process module in WebApp
120 └── tests/
121 ├── SplitApp.Modules.Users.Tests/
122 ├── SplitApp.Modules.Trips.Tests/
123 ├── SplitApp.Modules.Expenses.Tests/
124 ├── SplitApp.Shared.Messaging.Tests/ ← NEW: integration message contract tests
125 └── SplitApp.WebApp.IntegrationTests/ ← architecture invariants + HTTP smoke
126 ```
127
128 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.
129
130 ---
131
132 ## URL map
133
134 ### Webapp (port 97)
135 | URL | Purpose |
136 |---|---|
137 | `/` | Landing page (MVC) |
138 | `/Account/{Login, Register, Logout, Manage}` | MVC auth (HTTP → Users service → JWT cookie) |
139 | `/Trips`, `/Trips/{Create,Details/{id},Edit/{id},Delete/{id}}` | Trip CRUD |
140 | `/Members?tripId={id}` and `/Members/AcceptInvitation/{token}` | Trip participants + invitation flow |
141 | `/Expenses?tripId={id}` (with Create/Edit/Delete) | Trip expenses |
142 | `/Budget?tripId={id}` (CreateCategory/EditCategory/DeleteCategory) | Budget categories |
143 | `/Settlement?tripId={id}` | Balances + settlement plans |
144 | `/PollsClient?tripId={id}` (Create/Details) | Trip polls |
145 | `/WishlistClient?tripId={id}` | Trip wishlist |
146 | `/Admin/Dashboard` | Admin home (`admin` role) |
147 | `/Admin/Users` | Admin user list — proxies to `users-service` via HTTP |
148 | `/Admin/{Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist}` | Admin CRUD per entity |
149 | `/swagger` | Swagger UI — Trips + Expenses REST only |
150
151 ### Users service (port 98)
152 | URL | Purpose |
153 |---|---|
154 | `/swagger` | Swagger UI for all Users REST endpoints |
155 | `/health` | Healthcheck (200 only after migrate + seed completes) |
156 | `/api/v1/identity/account/{register, login, logout, refreshtokendata}` | Auth (anonymous) |
157 | `/api/v1/identity/admin/users` (GET/PUT/DELETE/+ `/{id}/roles`, `/admin/roles`) | Admin user CRUD (`[Authorize(Roles = "admin")]`) |
158
159 ### RabbitMQ management (port 15672)
160 - Login `guest` / `guest`
161 - Exchanges: `splitapp.events` (topic), `splitapp.requests` (direct)
162 - Queues: `q.webapp.UserDeletedEvent`, `q.users-service.GetUserByIdRequest`, `q.users-service.GetUsersByIdsRequest`
163
164 ---
165
166 ## Inter-service messaging
167
168 All cross-service communication goes through the `SplitApp.Shared.Messaging` library. Two patterns:
169
170 | Pattern | Used for | Library API |
171 |---|---|---|
172 | **Pub/Sub (events)** | Fire-and-forget fan-out (`UserDeletedEvent`) | `IMessageBus.PublishEventAsync<T>` + `IEventHandler<T>` |
173 | **RPC (requests)** | Synchronous data lookups (`GetUserByIdRequest`, `GetUsersByIdsRequest`) | `IMessageBus.RequestAsync<TReq, TResp>` + `IRequestHandler<TReq, TResp>` |
174
175 For the WebApp, `IUserLookup` is the high-level facade over the bus — controllers and services consume `IUserLookup`, not `IMessageBus` directly.
176
177 In-process MediatR is still used between **Trips ↔ Expenses** modules (they live in the same process). Only the Users module crossed the process boundary.
178
179 ---
180
181 ## Tests
182
183 ```bash
184 cd SplitApp.Modular
185 dotnet test
186 ```
187
188 **53 tests** across five projects, all passing:
189
190 | Project | Tests | Covers |
191 |---|---:|---|
192 | `SplitApp.Modules.Users.Tests` | 8 | `IdentityHelpers` — JWT generation/validation round-trips |
193 | `SplitApp.Modules.Trips.Tests` | 12 | `LangStr` — multi-language fallback, edge cases |
194 | `SplitApp.Modules.Expenses.Tests` | 10 | `CurrencyConverter` — exchange rates, rounding |
195 | `SplitApp.Shared.Messaging.Tests` | 9 | Wire-format contract round-trips (`GetUserByIdRequest`, `UserDeletedEvent`, `UserDto`, …) + marker interface invariants + queue naming |
196 | `SplitApp.WebApp.IntegrationTests` | 14 | Architecture invariants (module boundaries, schema isolation) + `WebApplicationFactory<Program>` HTTP smoke |
197
198 ---
199
200 ## Phase 4 ↔ Phase 3 mapping
201
202 | Phase 3 (modular monolith) | Phase 4 (microservices) |
203 |---|---|
204 | 1 process (`SplitApp.WebApp`) | 2 processes (`SplitApp.WebApp` + `SplitApp.UsersService`) |
205 | 1 Postgres database, 3 schemas (`users`/`trips`/`expenses`) | 2 Postgres databases (`splitapp` + `splitapp_users`) |
206 | In-process MediatR for all cross-module calls | RabbitMQ (`Users.*`) + in-process MediatR (`Trips`/`Expenses`) |
207 | `UsersDbContext` registered in `WebApp` host | `UsersDbContext` registered in `SplitApp.UsersService` only |
208 | `Areas/Identity` scaffolded Razor Pages handle login/register | New `Controllers/AccountController.cs` posts to Users service via HTTP, stores JWT in HttpOnly cookie |
209 | `Admin/UsersController` uses `UserManager<AppUser>` directly | `Admin/UsersController` uses typed `IUsersServiceClient` HTTP client |
210 | `[NotMapped] AppUser? CreatedBy` on Trip/Expense entities | `[NotMapped] UserDto? CreatedBy` — entities no longer reference Users domain types |
211 | ~13 WebApp files inject `UserManager`/`SignInManager`/`RoleManager` | All replaced with `User.FindFirstValue(ClaimTypes.NameIdentifier)` claim reads |
212 | 44 tests | 53 tests (added Shared.Messaging.Tests) |
213 | One container deploy (`docker compose -p splitapp-phase3 up`) | Five-container compose with healthcheck-gated startup |
214
215 ---
216
217 ## Known limitations
218
219 - **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.
220 - **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.
221
222 ---
223
224 ## Files & docs
225
226 - [architecture.md](architecture.md) — architecture deep-dive
227 - [SplitApp.Modular/README.md](SplitApp.Modular/README.md) — solution-level README
228