ABP Commercial Version: 8.3
Architecture: Monolithic
UI Framework: ASP.NET MVC
Database: SQL Server
License Type: Commercial
Executive Summary
We are operating a large-scale financial system experiencing significant transaction volumes with intensive read and write operations. We require official architectural guidance from the ABP team to optimize system performance, particularly for:
- Separating read operations from write operations to prevent performance degradation
- Isolating heavy reporting workloads from transactional processing
- Implementing appropriate caching strategies for financial data
Current Challenge
Our financial system processes a very high volume of daily transactions, and we're experiencing concerns regarding:
- Read-heavy operations (dashboards, lookups, financial queries) potentially impacting critical write operations
- Complex reporting queries degrading OLTP (Online Transaction Processing) performance
- Need to maintain optimal response times for core financial transactions under all conditions
Specific Questions
1️⃣ Read/Write Workload Separation
We seek ABP's official recommendations for:
- Architectural patterns to separate read and write workloads in ABP Commercial
- Whether ABP supports or recommends specific patterns (e.g., CQRS implementations)
- ABP-specific features or modules designed for this separation
- Best practices for implementing read replicas or read-specific data stores within ABP framework
2️⃣ Reporting Architecture
We need guidance on:
- Recommended approach for isolating reporting workloads from transactional operations
- Whether ABP Commercial includes built-in reporting optimization features
- Best practices for designing reporting databases/data warehouses alongside ABP
- Strategies for handling large-scale financial reports without affecting system performance
3️⃣ Caching Strategy
Please advise on:
- ABP Commercial's recommended caching approaches for financial data
- Which ABP caching mechanisms are most appropriate for high-frequency read operations
- Distributed caching configuration best practices for monolithic architecture
- Cache invalidation strategies that work well with ABP's Unit of Work pattern
4️⃣ Database Optimization
We would appreciate guidance on:
- ABP's recommendations for database indexing strategies for financial systems
- Entity Framework Core optimization techniques specific to ABP
- Connection pooling and query optimization best practices
- Whether ABP supports read-only database connections for query operations
What We're Looking For
- ✅ Official ABP Commercial best practices for high-volume financial systems
- ✅ Reference architectures or sample projects demonstrating these patterns
- ✅ Documentation covering performance optimization for large-scale ABP applications
- ✅ Module recommendations from ABP Commercial suite that address these challenges
- ✅ Configuration examples for optimal performance settings
Additional Context
- System must maintain ACID compliance for all financial transactions
- High availability and data consistency are critical requirements
- We prefer solutions that align with ABP framework architecture rather than custom implementations
- We are open to upgrading to newer ABP versions if specific features would benefit our use case
Expected Outcome
We aim to implement a production-ready architecture that:
- Ensures write operations are never impacted by read/reporting loads
- Provides fast response times for user-facing queries
- Scales efficiently as transaction volumes grow
- Follows ABP Commercial recommended practices and patterns
Thank you for your continued support. We greatly value the ABP team's expertise and look forward to your guidance.
Best regards,
1 Answer(s)
-
0
- Solution
- Read/Write separation (CQRS-friendly)
- Use CQRS at the application layer: create dedicated query application services (read side) and command application services (write side). Keep domain logic in domain services; keep queries out of domain services and preferentially in repositories or a separate query layer.
- Unit of Work: Commands run within ABP’s UoW by default; mark pure read methods as not transactional to avoid locking/overhead:
public class AccountQueryAppService : ApplicationService { private readonly IReadOnlyRepository<Account, Guid> _accountRepo; public AccountQueryAppService(IReadOnlyRepository<Account, Guid> accountRepo) { _accountRepo = accountRepo; } [UnitOfWork(IsTransactional = false)] public virtual async Task<AccountSummaryDto> GetSummaryAsync(Guid id) { var query = await _accountRepo.GetQueryableAsync(); var dto = await query.Where(x => x.Id == id) .Select(x => new AccountSummaryDto { Id = x.Id, Balance = x.Balance }) .FirstOrDefaultAsync(); return dto; } }- Read replicas: configure a read-only DbContext for queries via a separate connection string and DI registration. Use a custom repository or a second DbContext with [UnitOfWork(IsTransactional = false)] for read operations to direct them to the replica. Example:
[ConnectionStringName("ReadOnly")] public class ReadOnlyDbContext : AbpDbContext<ReadOnlyDbContext> { public DbSet<Account> Accounts { get; set; } public ReadOnlyDbContext(DbContextOptions<ReadOnlyDbContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder builder) { base.OnConfiguring(builder); builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); } } public class ReadOnlyAccountRepository : IReadOnlyRepository<Account, Guid> { private readonly ReadOnlyDbContext _dbContext; public ReadOnlyAccountRepository(ReadOnlyDbContext dbContext) { _dbContext = dbContext; } public async Task<IQueryable<Account>> GetQueryableAsync() => await Task.FromResult(_dbContext.Set<Account>().AsNoTracking()); }Commands continue to use the default write DbContext/connection. This isolates read traffic to replicas and protects OLTP writes.
Reporting architecture
- Offload heavy reports to background jobs or distributed events to prepare report data asynchronously and store in a reporting database/schema. Use ABP Background Jobs to schedule/execute report preparation without blocking transactional operations.
public class PrepareDailyLedgerJobArgs { public DateOnly Date { get; set; } } public class PrepareDailyLedgerJob : AsyncBackgroundJob<PrepareDailyLedgerJobArgs> { private readonly IReportProjectionService _projection; public PrepareDailyLedgerJob(IReportProjectionService projection) { _projection = projection; } public override async Task ExecuteAsync(PrepareDailyLedgerJobArgs args) { await _projection.BuildDailyLedgerAsync(args.Date); // read from OLTP, write into reporting store } }Use the Event Bus to publish domain events on transaction commit; a subscriber projects them into a reporting model (append-only or summarized aggregates). This keeps OLTP queries simple and reporting isolated.
Data stores: keep OLTP in SQL Server; create a separate reporting database (same SQL Server or separate instance). Point reporting UI to the reporting DB or read-only context.
Caching strategy (financial data)
- Use ABP’s caching abstractions (ICache<T>, IDistributedCache<T>) to cache read-heavy lookups, reference data, and computed aggregates that are safe to cache.
public class CurrencyRatesCacheItem { public DateTime AsOf { get; set; } public Dictionary<string, decimal> Rates { get; set; } = new(); } public class CurrencyRateAppService : ApplicationService { private readonly IDistributedCache<CurrencyRatesCacheItem> _cache; public CurrencyRateAppService(IDistributedCache<CurrencyRatesCacheItem> cache) { _cache = cache; } [UnitOfWork(IsTransactional = false)] public virtual async Task<CurrencyRatesCacheItem> GetRatesAsync() { return await _cache.GetOrAddAsync( "CurrencyRates:Current", async () => await LoadRatesAsync(), () => new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) } ); } }- Distributed cache: prefer Redis in production. Configure ABP to use Redis for IDistributedCache and ABP’s caching abstractions.
- Invalidation: tie cache invalidation to command paths using event handlers triggered after successful UoW commit. Invalidate minimal keys. For transactional consistency, invalidate after commit using IUnitOfWorkManager.Current?.OnCompleted(async () => { ... }) to avoid stale caches on rollback.
public class TransactionAppService : ApplicationService { private readonly IDistributedCache<AccountSummaryDto> _cache; private readonly IRepository<Transaction, Guid> _txRepo; private readonly IUnitOfWorkManager _uowManager; public async Task PostAsync(CreateTransactionDto input) { await _txRepo.InsertAsync(Map(input)); await CurrentUnitOfWork.SaveChangesAsync(); _uowManager.Current.OnCompleted(async () => { await _cache.RemoveAsync($"AccountSummary:{input.AccountId}"); }); } }Avoid caching mutable balances unless you can tolerate staleness for a short TTL; otherwise, cache reference data and computed report snapshots.
Database optimization
- Indexing: create composite indexes for your most frequent WHERE/ORDER BY patterns used by read queries; include columns that are selected often to cover queries. Keep indexes on write-heavy tables minimal and focused.
- EF Core with ABP:
- Use AsNoTracking for read queries; set global QueryTrackingBehavior.NoTracking for read-only DbContext.
- Use projection (Select) to DTO/anonymous types; avoid loading aggregates for read.
- Paginate consistently; avoid N+1 by joining/projection.
- Prefer compiled queries for ultra-hot paths if needed.
- Connection pooling: rely on ADO.NET pooling; ensure a bounded DbContext lifetime (scoped). Use Min/Max pool settings in connection strings only if necessary after measuring.
- Read-only connections: configure separate read-only connection string and DbContext as shown above; ensure the login/user has read-only permissions for that database to protect OLTP. Use [UnitOfWork(IsTransactional = false)] and AsNoTracking with that context.
Additional ABP recommendations that help at scale
- Background processing: move long-running, aggregating, and export tasks to Background Jobs. For higher throughput and visibility, integrate Hangfire with ABP (Commercial also supports it) and execute report builders there.
- Event-driven projections: use ABP’s distributed event bus to maintain reporting read models asynchronously; keep handlers idempotent and simple.
- Concurrency control: use ABP’s concurrency stamp (IHasConcurrencyStamp) on financial entities that may be edited via UI to prevent lost updates during parallel modifications.
Configuration checklist for your monolith
- Separate connection strings:
- Default: OLTP read/write
- ReadOnly: replica/secondary
- Register a read-only DbContext and repositories for queries.
- Add Redis and configure ABP to use distributed cache.
- Mark query services with [UnitOfWork(IsTransactional = false)] and use AsNoTracking.
- Move reports to background jobs, store results in reporting DB, and query the reporting DB from the UI.
- Add targeted indexes that match top queries for dashboards/reports.
- Separate connection strings:
- Sources
- https://abp.io/docs/latest/framework/architecture/modularity/basics
- https://abp.io/docs/latest/framework/infrastructure/distributed-locking
- https://abp.io/docs/latest/framework/infrastructure/background-jobs
- https://abp.io/docs/latest/framework/infrastructure/event-bus
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.