An enterprise-grade accounting backend where double-entry bookkeeping rules are enforced as domain invariants β not application-layer afterthoughts.
Unlike traditional CRUD-based portfolio projects, FinMind models accounting rules directly inside the domain to preserve business integrity.
Debits equal credits, or the transaction doesn't exist. That rule β along with every other accounting invariant β lives inside the domain model itself, enforced by rich entities with private setters and explicit behavior methods.
No application service can accidentally post an unbalanced journal entry β the domain itself refuses to transition an entry to Posted unless debits equal credits.
FinMind is a fully working general ledger, sub-ledger, and operations backend: chart of accounts, double-entry journal entries, customers, vendors, products with weighted-average costing, purchases, sales orders, and expense tracking.
All of it is wired together through a strict CQRS pipeline, covered by a real test suite, containerized, and shipped through a three-stage CI/CD pipeline to a live deployment.
- Core Features
- Architecture
- Tech Stack
- Project Structure
- Domain-Driven Design in Practice
- Screenshots & Demo
- Getting Started
- Running with Docker
- Testing
- CI/CD Pipeline
- Roadmap
- License
π General Ledger & Chart of Accounts
Full account lifecycle (Assets, Liabilities, Equity, Revenue, Expenses) with unique code enforcement and activation control.
π° Double-Entry Journal Engine
JournalEntry and JournalEntryLine enforce that every posted entry balances to zero before it can transition out of Draft. Entries move through Draft β Posted β Cancelled/Reversed with no shortcuts around the state machine.
π€ Accounts Receivable & Payable
Customer and Vendor entities are linked directly to dedicated GL accounts, so every sale or purchase has a natural home in the ledger from day one β no manual reconciliation required.
π¦ Inventory with Weighted-Average Costing
Product.ReceiveStock recalculates average cost on every incoming stock event:
AverageCost = ((CurrentQty Γ CurrentAvgCost) + (IncomingQty Γ PurchasePrice)) / TotalQty
π§Ύ Purchases, Sales Orders & Expenses End-to-end operational workflows that generate the journal entries behind them automatically, rather than requiring manual bookkeeping on top.
π’ Multi-Tenant & Fully Auditable
Every entity carries TenantId, CreatedAt/By, and UpdatedAt/By, with soft delete instead of destructive writes β nothing in the ledger ever silently disappears.
β‘ Domain Events State changes (e.g. an expense being completed) raise domain events for decoupled, delayed side effects instead of tangling handlers together.
FinMind follows Clean Architecture (Onion Architecture) strictly β dependencies point inward, and the Domain layer has zero knowledge of EF Core, ASP.NET Core, or anything infrastructural.
flowchart TB
A["Presentation Layer<br/>Controllers β MediatR"] --> B["Application Layer<br/>CQRS Features, Validators"]
C["Infrastructure Layer<br/>EF Core, Identity, JWT"] --> B
B --> D["Domain Layer<br/>Entities, Enums, Domain Events"]
C -.implements interfaces defined by.-> B
style D fill:#4c1d95,color:#fff
style B fill:#1e3a8a,color:#fff
style C fill:#374151,color:#fff
style A fill:#374151,color:#fff
The Domain layer stays completely independent of Infrastructure and the API β which keeps business rules testable in isolation and reusable behind any interface.
| Layer | Responsibility |
|---|---|
| Domain | Framework-independent core. Rich, self-validating entities (Account, JournalEntry, Product) that own their own business rules. |
| Application | Vertical-slice CQRS use cases. Depends only on Domain; exposes IUnitOfWork / IRepository<T> abstractions and never touches EF Core directly. |
| Infrastructure | Implements every Application-layer interface: AppDbContext, migrations, ASP.NET Core Identity, JWT issuance, seeders. |
| WepApi | Entry point. Thin controllers map HTTP β MediatR commands/queries; global middleware formats every error as RFC 7807 Problem Details. |
The payoff is practical: the entire accounting domain can be tested in complete isolation from the database, and the API layer could be swapped out entirely without touching a single business rule.
| Category | Technology |
|---|---|
| Framework | ASP.NET Core 9.0, C# 13 |
| Data Access | Entity Framework Core, SQL Server |
| CQRS / Messaging | MediatR |
| Validation | FluentValidation (as a MediatR pipeline behavior) |
| Auth | ASP.NET Core Identity, JWT Bearer Tokens, RBAC |
| Testing | xUnit, Moq, FluentAssertions, MockQueryable.Moq |
| Docs | Swagger / OpenAPI |
| Containerization | Docker, Docker Compose |
| CI/CD | GitHub Actions (3-stage: CI β Docker β Deploy) |
| Hosting | MonsterASP.NET (via MSDeploy) |
FinMind/
βββ Domain/ # Enterprise Domain Layer (zero external dependencies)
β βββ Common/ # BaseEntity, audit contracts, Result<T> wrapper
β βββ Entities/ # Account, Customer, Vendor, Product, JournalEntry...
β βββ Enums/ # EntryType, JournalEntryStatus, AccountType
β βββ Events/ # Domain events (e.g. ExpenseCompletedEvent)
βββ Application/ # Orchestration & Use-Case Layer
β βββ Common/Behaviours/ # MediatR pipeline behaviors (validation, etc.)
β βββ Features/ # Vertical-slice CQRS (Commands, Queries, DTOs, Handlers)
β βββ Interfaces/ # IUnitOfWork, IRepository, IJwtService
βββ Infrastracture/ # Technology & Data-Access Layer
β βββ Identity/ # ASP.NET Core Identity configuration
β βββ Persistence/ # DbContext, Repositories, Migrations, Seeders
βββ WepApi/ # Presentation / API Hosting Layer
β βββ Controllers/ # Thin routing controllers
β βββ Middlewares/ # Global exception handling
β βββ Program.cs # Bootstrap, DI registration, migrations on startup
βββ Tests/
βββ Domain.Tests/ # Business-rule and invariant unit tests
βββ Application.Tests/ # Command/query handler tests
FinMind avoids the anemic-model trap on purpose. State never mutates through a public setter β it mutates through a named, intention-revealing method:
public class Account : BaseEntity
{
public string Name { get; private set; }
public string Code { get; private set; }
public AccountType Type { get; private set; }
public bool IsActive { get; private set; }
public void DeActivate()
{
// Business rule enforcement happens here, not in a service layer
IsActive = false;
}
}// A journal entry can only be posted if debits equal credits β enforced by the domain itself
public Result Post()
{
if (TotalDebits != TotalCredits)
return Result.Failure("Journal entry is unbalanced.");
Status = JournalEntryStatus.Posted;
return Result.Success();
}Every command flows through a strict pipeline before it ever reaches a handler:
HTTP Request β Controller β MediatR β ValidationBehavior (FluentValidation)
β Command Handler β Domain Method β IUnitOfWork.SaveChangesAsync()
β Result<T> β Problem Details (on failure)
Live Deployment Pipeline
CI, Docker build, and deploy β all three GitHub Actions stages passing on main.
Test Suite
419 tests across the Application and Domain layers, zero failures.
API Surface (Swagger)
Every business domain exposed as its own controller group.
Database Schema
The full relational schema, including ASP.NET Core Identity tables.
Live Demo: http://finmind.runasp.net/
- .NET 9 SDK
- SQL Server (local, container, or remote)
- (Optional) Docker & Docker Compose
# Clone the repository
git clone https://github.com/<your-username>/FinMind.git
cd FinMind
# Restore dependencies
dotnet restore
# Update the connection string in WepApi/appsettings.json, then apply migrations
dotnet ef database update --project Infrastracture --startup-project WepApi
# Run the API
dotnet run --project WepApiMigrations and seed data (roles, financial settings) run automatically on startup β no manual seeding step required.
Once running, browse to /swagger for the full interactive API documentation.
FinMind ships with a multi-stage Dockerfile and a ready-to-use docker-compose.yml that spins up both the API and a SQL Server instance:
docker-compose up --buildThis starts:
sqlserverβmcr.microsoft.com/mssql/server:2022-latest, exposed on port1433with a persistent named volumewebapiβ built from the localDockerfile, connected to the SQL Server container via environment variables
The published image is also available on Docker Hub: mazenanter/finmind-api:latest.
The test suite is built on xUnit + Moq + FluentAssertions, structured with a strict Arrange-Act-Assert convention, and currently sits at 419 passing tests (294 in Application.Tests, 125 in Domain.Tests):
[Fact]
public void DeActivate_ShouldSetIsActiveToFalse()
{
// Arrange
var account = new Account("Cash", "1001", AccountType.Assets, 1);
// Act
account.DeActivate();
// Assert
Assert.False(account.IsActive);
}What's covered:
- β Command handler success and failure paths (duplicate codes, missing entities)
- β FluentValidation boundary checks (empty strings, invalid enums)
- β Domain event emission on state transitions
- β
Deferred
IQueryable<T>LINQ streams viaMockQueryable.Moq
Because domain entities are deliberately encapsulated with private setters, the test suite includes a reflection-based SetPrivateProperty helper to configure internal state (e.g. injecting a database-generated Id) without weakening the domain's public contract.
dotnet testFinMind ships through a three-stage GitHub Actions pipeline, fully automated from push to production:
push to any branch
β
βΌ
βββββββββββββββ build passes on main ββββββββββββββββ image pushed βββββββββββββββ
β CI (ci.yml) β ββββββββββββββββββββββββββΆ β Docker Build β ββββββββββββββββββΆ β Deploy β
β build + test β β (docker.yml) β β(deploy.yml) β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
dotnet test on pushes image to win-x86 build,
.NET 9 / Ubuntu Docker Hub MSDeploy to
MonsterASP.NET
- CI β runs on every push: restores, builds, and executes the full test suite on a .NET 9 / Ubuntu runner with NuGet caching.
- Continuous Delivery β on a successful
mainbuild, builds the Docker image and pushes it to Docker Hub. - Continuous Deployment β on a successful image build, compiles a Windows (
win-x86) release and deploys it live via MSDeploy.
No manual deployment step exists in this project β a merge to main is a production release.
- Multi-Tenant Architecture
- LLM-powered financial insights and anomaly detection on posted entries
- Financial statement generation (Trial Balance, Income Statement, Balance Sheet)
- Docker image optimization & multi-arch builds
This project is licensed under the MIT License.
Built by Mazen Anter β Backend .NET Developer
If this project is useful to you, consider giving it a β