# LendCore Scalability and Multi-User System Review

## 1. Purpose and scope

This document assesses the current LendCore design for a growing multi-user loan platform. It focuses on database behavior, access control, financial consistency, queueing, reporting, operational constraints, and likely update areas.

This is a documentation-only assessment. It does not change application code, migrations, routes, configuration, or data. The migration files remain the schema authority; the live database may differ until migrations are applied and verified.

The review covers the repository state inspected on 23 August 2026, including:

- Customer, administrator, and officer web surfaces.
- Sanctum-protected mobile/API routes.
- Loan, repayment, transaction, allocation, suspense, guarantee-deposit, audit, and settings tables.
- M-Pesa callback and queue processing paths.
- Eloquent models, selected controllers, middleware, migrations, and queue configuration.

## 2. Executive assessment

LendCore has a sound modular domain shape for a small-to-medium deployment: users own loans, loans produce repayment schedules, transactions represent financial events, and payment allocations preserve how money was distributed. Role-specific web areas and Sanctum API authentication provide a useful starting boundary.

The current design is not yet a demonstrated high-volume or high-concurrency platform. The main scale limit is likely to be the shared MySQL workload rather than PHP request handling. Payment callbacks, repayment allocation, dashboard aggregates, exports, Telescope data, and database-backed queue jobs all compete for the same database resources. As user and transaction counts grow, unbounded reads, relationship loading, repeated aggregate calculations, missing or incomplete composite indexes, duplicate callback handling, and long-running exports are likely to become the dominant risks.

The safest growth path is staged:

1. Establish measurements, ownership tests, data-retention rules, and database/index baselines.
2. Make payment and lifecycle operations explicitly idempotent and concurrency-safe.
3. Move heavy reporting, imports, callbacks, and exports away from synchronous web requests.
4. Introduce read models or reporting replicas only after query and workload evidence justifies them.
5. Partition or archive very large event tables only after retention and reconciliation rules are defined.

## 3. Current system shape

### 3.1 Primary actors

| Actor | Current access mechanism | Main workload |
| --- | --- | --- |
| Customer | Web `auth` plus `user` middleware; API Sanctum token | View own loans and repayments, apply, pay fees, initiate repayments, view transactions |
| Administrator | Web `auth` plus `admin` middleware; selected API callback-registration routes add `admin` | Manage products, users, loans, payments, fines, deposits, suspense, reports, exports, and settings |
| Officer | Web `auth` plus `OfficerMiddleware` | View assigned-user dashboards, users, loans, and repayments |
| M-Pesa provider | Public callback endpoints | Send STK, B2C, C2B, and non-STK callback payloads |
| Queue worker | Laravel queue connection | Process deferred C2B non-STK confirmations and related financial work |

The role middleware checks the authenticated user's role value. This is useful for surface separation, but it is not a complete object-level authorization model. Every customer, officer, administrator, callback, and model-binding path still needs an explicit ownership or permitted-scope decision.

### 3.2 Financial data chain

```text
User
  -> LoanRequest -> Loan -> Repayment -> Fine
  -> Transaction -> PaymentAllocation -> Repayment
  -> SuspenseTransaction when a payment cannot be matched

Loan/User -> GuaranteeDeposit
Administrative and callback events -> AuditLog
```

The separation between `transactions` and `payment_allocations` is a strength. It preserves both the incoming financial event and its eventual distribution. It also creates a consistency obligation: every allocation, repayment balance, loan balance, and transaction status must be updated atomically or reconciled safely.

## 4. Database scalability review

### 4.1 Strengths

- Core ownership foreign keys exist for users, tiers, loans, repayments, transactions, suspense records, and guarantee deposits.
- Important monetary columns use decimal database types rather than floating-point database types.
- The schema includes unique email, personal access tokens, suspense transaction ID, and several operational indexes.
- Payment allocations provide an auditable distribution ledger instead of collapsing all payment information into a repayment row.
- Status fields make operational queues and lifecycle reports possible.
- Nullable links on transactions and suspense records support real callback and reconciliation scenarios.

### 4.2 Confirmed constraints and probable pressure points

#### A. Index coverage is uneven

Several high-frequency filters are likely to need composite indexes as data grows. The migration set visibly indexes some foreign keys, but not every common combination used by operational queries. Candidate areas for measurement and later patching include:

- `loans`: `(user_id, status, created_at)`, `(status, created_at)`, and `(tier_id, status)` where reports or queues use them together.
- `repayments`: `(loan_id, due_date)`, `(status, due_date)`, and possibly `(loan_id, status)`.
- `transactions`: `(user_id, created_at)`, `(loan_id, status, type)`, `(mpesa_transaction_id)`, `(checkout_request_id)`, and callback correlation fields.
- `payment_allocations`: combinations used for transaction, loan, repayment, and M-Pesa reconciliation.
- `users`: `(role, status, created_at)` and officer assignment/status combinations.
- `audit_logs`: `(resource_type, resource_id, created_at)` and `(admin_id, created_at)`.
- `notifications`: `(notifiable_type, notifiable_id, created_at)` if not already supplied by the framework migration.

These should be added only after checking actual query plans and existing indexes. Redundant indexes increase write cost and storage.

#### B. Some application reads are unbounded

The inspected controllers use pagination in many admin lists, which is positive, but other paths use `get()` for complete user loan lists, repayment lists, tier lists, report datasets, suspense/direct transaction datasets, and relationship collections. This is acceptable at low volume but creates memory, response-time, and database pressure when one customer, officer, report period, or administrative result set becomes large.

Likely patch areas:

- Replace large interactive result sets with bounded pagination or cursor pagination.
- Select only columns needed by each screen.
- Define explicit limits for export and search endpoints.
- Stream or queue large exports instead of constructing the entire dataset in a web request.
- Avoid loading nested repayments, fines, and transactions for a whole list when the screen needs only summaries.

#### C. Aggregated model attributes can multiply queries

`Loan` calculates values such as total repaid and arrears through relationship queries. These are convenient and readable, but invoking them inside a collection, dashboard, report, or loop can create repeated aggregate queries. The risk is an N+1 pattern and repeated scans of repayments or transactions.

Likely patch areas:

- Use explicit aggregate queries such as `withSum`, grouped summaries, or dedicated reporting queries.
- Load calculated values once per request where possible.
- Establish a clear rule for when denormalized balances are authoritative and how they are reconciled.
- Add query-count and response-time tests for dashboards and reports.

#### D. Event and observability tables can grow faster than domain tables

`transactions`, `payment_allocations`, `audit_logs`, `login_activities`, notifications, jobs, failed jobs, and Telescope entries are append-heavy or operationally persistent. Without retention, archival, or cleanup policies, they will eventually dominate storage and index maintenance.

Required operational decisions:

- Retention periods for Telescope, login activity, notifications, and completed jobs.
- Regulatory and financial retention requirements for transactions, allocations, suspense records, and audit logs.
- Archive format, restore procedure, and reconciliation evidence before deleting anything.
- Whether callback payloads should remain in hot tables or move to controlled archival storage.

#### E. Migration history needs deployment discipline

The repository contains incremental and duplicate-safe-looking migrations for Telescope and queue tables, as well as many later additions to core tables. This is normal in an evolving system, but production deployments must run migrations once, in order, with backups and schema verification. A new deployment should not assume that the live schema matches a clean migration install.

Before a major scale update, record:

- Migration status and checksum/version inventory.
- Actual indexes, foreign keys, column types, and row counts in production.
- A rollback or forward-fix plan for every schema change.
- Whether old columns remain required by import or legacy accounting paths.

#### F. Decimal storage does not eliminate arithmetic risk

Database money fields use decimal types and models cast many values as `decimal:2`, which is appropriate. However, some model accessors convert values to PHP floats for calculations. This can introduce rounding ambiguity at operation boundaries, especially when several payments are allocated or when balances are compared.

Future financial patches should define one rounding policy, use decimal-safe arithmetic in services, record before/after values, and reconcile totals from the ledger. Do not silently change historical totals while correcting arithmetic.

## 5. Access control and multi-user review

### 5.1 Existing strengths

- Web routes are grouped by authenticated role surfaces.
- API business routes are protected by Sanctum.
- Customer API controllers generally scope list queries to the authenticated user.
- Officer assignment is represented directly on `users`, allowing indexed ownership queries and operational assignment screens.
- Admin actions and important system events have an audit-log concept.

### 5.2 Constraints requiring explicit verification

#### A. Role checks are coarse-grained

The middleware answers: “Is this user an admin, customer, or officer?” It does not answer: “May this actor access this specific loan, repayment, transaction, user, or officer assignment?” That decision must be present in controllers, services, policies, scoped bindings, or query constraints.

High-priority access tests should cover:

- A customer requesting another customer's loan, repayment, or transaction by ID.
- An officer requesting an unassigned user's records by ID.
- An officer changing data through any route that was intended to be read-only.
- An API token belonging to a customer invoking an admin-only operation.
- A user whose role or status changes while an existing session or token remains active.
- Model binding with soft-deleted, reassigned, or legacy records.

The officer route inventory says officers can view users and details broadly, while one inspected query scopes the list by `assigned_officer_id`. This mismatch should be resolved as a documented business rule and enforced consistently.

#### B. Public integration routes require perimeter controls

M-Pesa callback endpoints and the web-triggered queue-processing endpoint are outside the Sanctum-authenticated group. Provider callbacks must be reachable, but they should be protected by provider verification, source/network controls where available, authenticated secrets or signatures, strict payload validation, rate limiting, idempotency, and monitoring. The queue-processing endpoint needs especially careful review because it can trigger server-side work without a customer token.

This document does not assume that such controls are absent at the web server or provider perimeter; it identifies them as deployment requirements to verify.

#### C. Sensitive data exposure needs a logging policy

Callback payloads, request data, headers, M-Pesa identifiers, phone numbers, and audit information are operationally useful but sensitive. Debug and application logs should have redaction rules, access controls, retention limits, and an incident response path. Telescope should not be enabled broadly in production without reviewing payload and credential exposure.

#### D. Sessions and tokens need lifecycle rules

The database-backed session and Sanctum token tables can scale, but they need expiration, revocation, cleanup, and indexing policies. Role/status changes should have an explicit effect on active sessions and tokens. Login activity retention should not become an accidental source of indefinite personal-data storage.

## 6. Financial concurrency and correctness

This is the highest-risk area because a performance improvement that duplicates or loses money is not an acceptable improvement.

### 6.1 Required invariants

The system should be able to prove these invariants for every payment:

1. One provider transaction ID cannot create two financial transactions.
2. One checkout request cannot create multiple effective payment records unless the business rule explicitly allows it.
3. Total allocation for a transaction cannot exceed the transaction amount.
4. A repayment balance cannot become negative because of concurrent requests.
5. A loan cannot be completed before all required payable amounts are settled.
6. A disbursement cannot be applied twice after callback retries or timeout/result races.
7. A suspense record can be reassigned and applied once, with an audit trail.
8. A failed callback can be retried without changing the financial result.
9. Manual and automated payment paths use the same accounting and reconciliation rules.

### 6.2 Current design risks to test and patch

The queued non-STK confirmation job first checks whether an M-Pesa transaction exists and then processes it. That is a useful idempotency intention, but a check-then-act sequence can race when the same callback is delivered concurrently. A unique database constraint and an atomic transaction are stronger than an application-level existence check alone.

The payment/allocation services must also be reviewed for row locking or equivalent conflict handling around the affected transaction, loan, repayment, and suspense row. The inspected job uses retries and a database queue, but retrying a financial operation is safe only when the operation is idempotent.

The database queue is configured as the default and uses `after_commit => false`. This setting deserves explicit review: a job dispatched inside a database transaction may be visible to a worker before the transaction commits, allowing the worker to observe incomplete state. Financial jobs should have a deliberate commit-order policy.

### 6.3 Recommended correctness test matrix

- Duplicate identical callback delivered simultaneously.
- Same payment callback delivered after a timeout response.
- Two payments arriving for the same repayment at the same time.
- Payment and administrator completion/default action racing.
- Suspense reassignment racing with callback retry.
- Queue worker crash after transaction creation but before allocation.
- Database deadlock followed by a safe retry.
- Partial payment, overpayment, fine capture, guarantee-deposit offset, and rollover in concurrent scenarios.

## 7. Queueing and asynchronous workload

### Current position

The application supports database, Redis, SQS, Beanstalkd, sync, and other Laravel queue drivers, with the database connection as the default. The non-STK C2B confirmation path implements retries and a timeout. Composer's development command starts a queue listener, while production scale will require supervised, observable worker processes.

### Constraints

- Database queues are simple and reliable for low throughput but compete with business tables for database locks, I/O, and connections.
- A single queue name can allow slow or failing work to delay unrelated work.
- Long-running imports, exports, callback processing, and notifications should not share an undifferentiated queue under load.
- Retry counts and retry-after/timeout values must be compatible. A worker must not start a duplicate attempt while the previous attempt is still running.
- Failed jobs need alerting and replay procedures, not only storage.

### Patch direction

Define separate queues by workload and business criticality, for example payments, disbursements, imports, reports, notifications, and maintenance. Use a durable broker appropriate to expected throughput, supervise workers, expose queue depth and age metrics, and document dead-letter/replay behavior. Financial jobs should record an operation key and final outcome so a replay is provably safe.

## 8. Reporting, imports, and administrative load

Reporting and legacy import are likely to compete with customer and payment traffic because they scan large portions of the same tables. Reports that combine transactions, repayments, allocations, suspense records, fines, and deposits can become expensive even when each individual query is reasonable.

Probable patch areas:

- Use bounded date ranges and mandatory filters for large reports.
- Use indexes that match the report's actual predicates and sort order.
- Run Excel/PDF generation asynchronously and store an expiring download artifact.
- Use chunked reads for imports and exports.
- Make import batches resumable, observable, and idempotent.
- Separate operational dashboards from historical aggregate reporting.
- Consider daily/monthly summary tables or a read replica when query evidence supports them.
- Prevent administrative exports from exhausting the PHP worker or database connection pool.

A reporting replica is not a substitute for correct accounting. Reports must declare their freshness and reconcile to the transaction/allocation ledger.

## 9. Application and deployment scalability

### Web/API tier

Laravel's request layer can scale horizontally when sessions, cache, queues, and uploaded/exported artifacts are externalized or shared. The database remains the coordination point for financial operations. Every added web node must use the same application key, compatible configuration, shared cache/session strategy, and consistent release.

### Cache

Tier lists, stable settings, and non-financial reference data may be cacheable. Customer balances, repayment status, fee payment state, and callback outcomes require explicit invalidation or short-lived reads. Never use stale cache as the accounting authority.

### Database

Track connection utilization, slow queries, lock waits, deadlocks, buffer/cache hit behavior, table growth, index size, and replication lag. Capacity planning should include peak callback bursts, repayment due-date traffic, import windows, and report/export activity, not only average concurrent users.

### Files and backups

PDF/Excel outputs, logs, imports, and database backups need storage lifecycle rules. Backups should be encrypted, tested by restore, and separated from the primary host. A backup that has never been restored is not a verified recovery plan.

## 10. SWOT analysis

| Strengths | Weaknesses |
| --- | --- |
| Clear loan-to-repayment domain model. | Shared database is likely to carry web, callbacks, queues, and reports together. |
| Decimal database columns for monetary values. | Some PHP accessors convert money to floats. |
| Separate transaction and allocation ledgers. | Idempotency and locking need proof under concurrent callback delivery. |
| Role-separated web surfaces and Sanctum API protection. | Role middleware alone does not provide object-level authorization. |
| Pagination exists on many admin lists. | Other controller paths and relationship loads use unbounded `get()` patterns. |
| Queue driver abstraction and retry-capable callback job. | Database queue and `after_commit` behavior require production hardening. |
| Suspense workflow provides a recovery path for unmatched payments. | Suspense and callback duplicate/retry rules need strong database guarantees. |
| Audit, Telescope, and login activity support observability. | Operational tables can grow indefinitely without retention and privacy rules. |

| Opportunities | Threats |
| --- | --- |
| Add measured composite indexes and query budgets. | Duplicate callbacks or concurrent payments causing financial misallocation. |
| Introduce idempotency keys and atomic financial operations. | Database lock contention during due-date or callback bursts. |
| Split queues and supervise workers. | Reports/imports starving customer and payment traffic. |
| Add read models or reporting replicas. | Sensitive callback, token, or personal data leaking through logs/telemetry. |
| Formalize policies and ownership tests. | Role/status changes leaving sessions or tokens over-privileged. |
| Archive event-heavy tables with reconciliation. | Migration drift between clean installs and production databases. |
| Add load, fault-injection, and reconciliation testing. | M-Pesa/provider outages, delayed callbacks, and retry storms. |
| Move exports to asynchronous artifacts. | Unbounded exports and N+1 queries exhausting memory or workers. |

## 11. Prioritized patch and update roadmap

### Priority 0: before materially increasing traffic

- Establish database backup/restore verification and migration-drift checks.
- Inventory actual production indexes, foreign keys, row counts, and slow queries.
- Test customer and officer object-level access for every identifier-based endpoint.
- Verify callback authentication, queue endpoint exposure, rate limits, and payload validation.
- Define financial invariants and reconciliation reports.
- Add concurrent duplicate-callback and duplicate-payment tests.
- Define log redaction and Telescope production policy.

### Priority 1: correctness and predictable load

- Make provider transaction identifiers and operation keys enforceably unique where business rules permit.
- Make payment, allocation, disbursement, suspense reassignment, and lifecycle transitions atomic and concurrency-safe.
- Review queue dispatch timing relative to database commits.
- Replace high-volume unbounded reads with pagination, cursor pagination, chunking, or streaming.
- Add query plans and composite indexes based on measured workload.
- Separate payment, import, report, and maintenance queues.

### Priority 2: operational scale

- Add queue depth/age, callback latency, lock wait, deadlock, database connection, slow-query, and reconciliation metrics.
- Queue large imports and exports with progress, cancellation, retry, and expiry behavior.
- Introduce retention and archive jobs for Telescope, logs, login activity, notifications, jobs, and other event-heavy tables.
- Use aggregate queries or read models for dashboards instead of repeated per-loan relationship calculations.
- Test worker restarts, provider retries, database failover, and partial job completion.

### Priority 3: larger user and transaction populations

- Evaluate Redis or a managed durable queue broker if database queue contention is demonstrated.
- Evaluate read replicas for reporting and read-heavy portal traffic.
- Introduce partitioning or archive storage for transactions, allocations, audit logs, and callback payloads only after a tested retention design.
- Consider service separation for high-volume payment ingestion only when operational maturity and measured bottlenecks justify the added complexity.

## 12. Capacity planning model

Capacity should be expressed in business events, not only registered users. Track at least:

- Registered, active, pending, officer, and administrator users.
- Loans per user and active loans per user.
- Repayments per loan.
- Payment transactions and allocations per day, including peak burst rate.
- Callback retries and unmatched/suspense rate.
- Daily import volume and largest import batch.
- Report/export requests by duration and row count.
- Queue depth, oldest job age, retry count, and failed-job rate.
- Database storage growth and index growth per month.

A useful load test should model ordinary traffic plus a callback burst, a due-date repayment burst, one large administrative report, and a concurrent import. Success criteria should include latency, error rate, queue age, lock waits, financial reconciliation, and absence of duplicate ledger entries.

## 13. Definition of operational readiness

LendCore should be considered ready for a larger multi-user deployment only when the team can demonstrate:

- Every user-facing record lookup enforces the intended ownership or role scope.
- Every payment and disbursement operation has a stable idempotency key and a replay-safe outcome.
- Duplicate callbacks and worker retries do not duplicate money movement.
- Financial tables reconcile without unexplained differences.
- Heavy reports/imports/exports do not block payment processing.
- Queue and database saturation are visible before users report failure.
- Backup restore, migration rollout, rollback/forward-fix, and archive restoration have been exercised.
- Personal and financial data is redacted from ordinary logs and retained only as long as justified.
- Capacity limits and freshness expectations are documented for dashboards, reports, and APIs.

## 14. Source basis and limitations

This assessment is based on the repository documentation, migration files, selected middleware, routes, models, controllers, queue job, queue configuration, and composer scripts. It is not a substitute for production query plans, traffic metrics, lock/deadlock traces, penetration testing, or a full line-by-line authorization audit.

The next evidence-gathering step should be a production-like staging exercise that captures query plans, row counts, queue behavior, concurrent payment outcomes, and access-control test results. Recommendations should then be promoted into implementation tickets with a migration plan, rollback/forward-fix plan, test cases, and measurable acceptance criteria.
