Jan 2025 – Mar 2026
DDMind
Private equity research and investment analysis platform
An AI-assisted platform for researching equities, debt and company financials, replacing data kept in heavy Excel workbooks. I was main developer, led two developers, and worked directly with the client on requirements and estimates.
- Role
- Main developer, led 2 developers
8–10 s
faster grid load
20,000+
financial records
−12%
bundle size
−8%
build time
Problem
Tables held 20,000+ financial records, with 17,000+ rows in a single table. The grid loaded every page at page load, took a long time to become usable, and froze after users loaded several files.
The app was also behind on Angular versions, and the client was pursuing a certification that ruled out CDN-loaded assets and deprecated or outdated dependencies.
Constraints
- Pre-production product in its funding stage: requirements moved as the client refined the product.
- The client wanted every one of roughly 30–40 tabs to feel instant when clicked.
- The app would be sold as SaaS, so limits like upload counts had to be configurable per organization.
Architecture
What I built
Two-layer grid fix
On the data side, I moved to server-side pagination: only the page in view is fetched, and an unvisited page is fetched on demand. On the rendering side, I used AG-Grid's infinite row model and row virtualization, and cached fetched pages on the client.
Grid customization
API responses carried custom data that needed its own markup, so I extended the grid at runtime with custom cellRenderers. Per-table helper buttons change decimal places and run calculations on numeric cells only, through valueFormatter and valueGetter, and leave other cells untouched.
Config-driven tabs with background prefetch
An API response defines 5–6 main tabs with 6–7 sub-tabs each, every one holding tables, charts and content. Main content loads first; each tab's data is then prefetched in the background with a delay and cached, so switching tabs reads from cache.
Angular v16 → v20 upgrade
I proposed and led the upgrade: standalone components, the new control flow, OnPush and trackBy where lists re-rendered, and removal of unused code, packages and libraries.
Streaming LLM chat
A WebSocket chat with streaming responses lets users ask direct questions about a company or report instead of reading it end to end. OpenAI and Anthropic are switchable, with provider routing on the backend.
Reusable charts, state and uploads
One config-driven AG Charts component powers almost every chart. State uses NGXS and NgRx where needed and RxJS BehaviorSubject services at global, module and component level. Files upload directly to AWS S3 through pre-signed URLs (up to 50 MB each, 10–25 files per organization).
Reverse-engineered data flow
I studied how a reference product processed market data and replicated the behaviour on the frontend, shaping request and response structures so the backend could call the same market-data APIs consistently.
Decisions and trade-offs
Prefetch tab data instead of loading on click
Choice: Load main content first, then fetch every tab's data in the background with a delay and cache it.
Trade-off: More backend calls for tabs some users never open. Accepted because instant tab switching was the client's priority.
Lazy-load almost everything
Choice: Nearly every module is lazy-loaded, and heavy features were split into sub-modules.
Trade-off: Tab data is the deliberate exception: components stay lazy, but their data is prefetched.
Direct-to-S3 uploads
Choice: The frontend uploads straight to S3 with a pre-signed URL from the backend.
Trade-off: Upload traffic never passes through the API server, at the cost of handling URL expiry and per-org limits on the client.
Results
- Grid load time cut by about 8–10 seconds, and the freezes after multiple file loads stopped.
- Bundle size down 12% and build time down 8% after the v20 upgrade, with deprecated dependencies removed for the client's certification.
- Quality gates (ESLint and Prettier on pre-push, the same checks in GitHub Actions) and a Dockerized dev environment.
What I'd do differently
- Record Lighthouse and Web Vitals numbers before and after every optimization. I checked performance with Lighthouse and DevTools but did not keep the scores.
- With Angular 17+, @defer with prefetch-on-idle can handle tab components, and prefetching only the next likely tabs would cut unneeded calls.
Next case study
CCM →