Open Closed

Architectural Guidance Request: Performance Optimization for High-Volume Financial System #10105


User avatar
0
VertoCore created

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:

  1. Separating read operations from write operations to prevent performance degradation
  2. Isolating heavy reporting workloads from transactional processing
  3. 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)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. 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.
    1. 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.

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.1.0-preview. Updated on November 11, 2025, 06:29
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.