GSoC: getting in, getting stuck, and building a loan-risk engine
Why I chose Mifos, what helped me get selected, and what I learned building a reactive loan-risk assessment microservice during GSoC 2025.
My motivation for participating in Google Summer of Code was pretty straightforward: I wanted to become a better software engineer.
I knew the direction I wanted to grow in. Fintech interested me, and I wanted it to become a domain I understood well. I also wanted experience building a service that interacted with other systems—internal services, third-party APIs, databases, the whole thing.
So while choosing a GSoC project, I was also choosing what I wanted to learn.
I wanted to push myself beyond the kind of projects I was already comfortable with. Building a microservice that had to coordinate several moving parts felt like a good way to do that.
That eventually led me to the Mifos Initiative, where I spent GSoC 2025 building a reactive loan-risk assessment engine.
I talked about how I got started and selected in this podcast. The video starts at the GSoC discussion; the rest of the episode covers other topics.
But before I could build anything, I had to get the existing application running.
That deserves its own section. lol.
First challenge: make the application run
I started exploring organizations in December. I looked through previous GSoC projects, with Java already in mind as the stack I wanted to work with. Mifos caught my attention because it developed banking software. It gave me a chance to learn the technology and the financial workflows behind it.
By February, I was trying to set up the application locally.
“Trying” is doing some heavy lifting there.
I was using Arch Linux at the time, and getting the frontend, backend, and database running together was a struggle. During that process, I switched to Ubuntu and upgraded my RAM from 16 GB to 48 GB. The setup phase took me around a month.
I’m mentioning this because looking at a large codebase from the outside can be intimidating. There are unfamiliar modules, configuration files, dependencies, and errors that don’t mean much to you yet. You haven’t even changed anything, and somehow things are already broken.
For me, a lot of that initial friction eased once I could run the application and follow its flows.
I could perform an action in the frontend, see the request reach the backend, and start connecting the code to something concrete. The system became easier to understand a little at a time.
Getting it running didn’t mean I suddenly understood everything. It gave me somewhere to start.
My first PR didn’t get merged
I started contributing around the first week of March.
My first pull request didn’t get merged. Another contributor was working on the same issue, and their solution was merged instead.
A slightly disappointing start, obviously. But it also made me think about how I was approaching contributions.
It’s easy to turn GSoC preparation into a PR-counting exercise. You see other people contributing and start wondering whether you’ve done enough.
I found it more useful to focus on the problems I was solving. Was I finding something that mattered? Did I understand why it was happening? Could I explain my fix?
That became a much better way to spend my time.
Finding issues was part of the contribution
The web app was in a pre-release stage, so there were bugs to investigate.
I started attending the community’s Friday calls. I would share my screen, show the issues I’d found, and walk through what I was working on and how I was approaching it.
Those calls helped me get to know the mentors. They also gave me a way to demonstrate my understanding while it was still developing. I could discuss a problem across the frontend and backend, explain what I had figured out, and ask about the parts I hadn’t.
I reported issues on Jira and often picked up the more involved ones myself. Other contributors took up some of the issues I reported, and I helped clarify the behaviour or walked them through the relevant parts of the application. Sometimes that included helping them get their own setup working.
I was asking for help too. Quite a bit of it.
Looking back, I think this helped me make the cut: I was finding issues that weren’t immediately obvious, proposing fixes, and following through on them—all while learning the domain and becoming more comfortable with the code.
I can’t tell you exactly how the selection decision was made. But I think that work gave the community a useful picture of how I would approach the project.
Learning to ask useful questions
One of the most useful things I learned was how to ask questions that someone could actually answer.
When you’re new, “I don’t understand this project” is a very real feeling. It’s also difficult for someone else to respond to.
Trying things first helped me narrow down what I needed to ask. I could explain what I was trying to do, what I expected, what happened instead, and where I had already looked.
That gave the conversation somewhere to go.
It also helped me understand my own confusion. Sometimes, by the time I had put together a clear question, I had already worked out part of the answer.
I still needed help. I was just getting better at making that help useful.
Writing the proposal
I spent around fifteen days working on my proposal and got feedback from three mentors.
By then, I had spent time with the application and the community. That gave me more context for thinking through the project: what needed to be built, how it would fit into the existing system, and what I would need to learn.
Kafka and Spring WebFlux were new to me. I had to read, watch explanations, and work through the architecture while preparing the proposal.
I also laid out the work week by week and discussed my availability with the mentors. That forced me to turn a broad idea into something I could actually plan.
Getting selected was exciting. Then came the part I had signed up for: building the thing.
What the loan-risk engine needed to do
A loan application can involve several checks. For my project, the implemented integrations focused on bank-statement analysis and credit-bureau pulls.
Those checks depend on information held in different places. The loan lives in the core system. Documents may be uploaded separately. External providers process those documents or return credit information.
The service had to bring those pieces together.
It needed to listen for changes in Apache Fineract, fetch the relevant documents, decide when a loan was ready for analysis, call external providers, and store the results for inspection.
A simplified view of the flow looked like this:
Apache Fineract
Loan events + document events
│
▼
Kafka
│
▼
Event persistence + dispatch
│
▼
Loan state + document handling
│
▼
Readiness checks
│
┌────┴────┐
▼ ▼
Bank-statement Credit-bureau
analysis pull
│ │
└────┬────┘
▼
Persisted results
│
▼
Dashboard + report views
The broad architecture was easy enough to draw. The interesting work was deciding what should happen at each step, especially when something was missing or failed.
The details are also documented in my GSoC final report.
Following an event through the system
The service consumed loan and document events from Fineract’s Kafka external-events topic.
I used Spring WebFlux for the reactive application and R2DBC to work with PostgreSQL. Liquibase managed the database schema.
Incoming events were persisted in an event_message table, which tracked their processing state. A dispatcher then routed them to the relevant handler.
Loan handlers dealt with events such as loan creation, modification, withdrawal, and rejection. Document handlers dealt with document creation and deletion.
Persisting the event gave the service a record of what it had received and a basis for reprocessing work that hadn’t completed. That became important once failures and retries entered the picture.
There was also work to do upstream. I contributed changes to Apache Fineract to emit events for document uploads and deletions, as well as loan modifications and applicant withdrawals.
A consumer can only react to changes it hears about. Part of building this service was making sure those changes were being announced.
Having a loan didn’t mean it was ready
One of the problems I ran into was triggering analysis before the required documents were available.
A loan could exist in the system while its supporting documents were still being uploaded. Receiving a loan event was therefore not enough reason to call a bank-statement analysis provider.
I brought the readiness checks together in an aggregator, which maintained the relevant state for each loan and determined whether downstream services could run.
On a document-created event, the service fetched the attachment from Fineract using a reactive WebClient. It stored the file through a pluggable ObjectStorageClient, initially backed by the local filesystem, and recorded its metadata.
That metadata connected the stored document to the loan and tracked its status.
The distinction mattered: knowing that a document had been uploaded was different from having successfully fetched and stored the file needed for analysis.
The aggregator gave those pieces a place to come together before external calls were triggered.
Integrating providers without spreading their details everywhere
The first bank-statement analysis integration used Arya AI. The credit-bureau integration used iSoftPull.
Each provider had its own API, request format, and response structure. I introduced service provider interfaces, or SPIs, so the orchestration code could work through a common contract.
Provider-specific implementations handled the translation between the service’s internal models and the external API.
For bank-statement analysis, the service submitted documents, correlated requests, and persisted analysis results. It also exposed the provider’s PDF report when available.
For credit-bureau pulls, the implementation mapped requests and responses, stored normalized JSON, and made the detailed report link available. The integration could also be enabled or disabled through configuration.
That separation gave future integrations a clearer place to fit. Adding another provider would still require implementation work, but its API details could stay within its adapter instead of spreading through the loan workflow.
The reactive part
I wanted this project to teach me how to build a service that interacted with several other services. It definitely delivered on that.
External calls involve waiting. So do document downloads and database operations. The reactive stack let me compose those operations without holding a request thread blocked through each wait.
Where checks were independent and ready to run, the service could fan them out in parallel.
But using a reactive stack didn’t remove the need to think about limits. Calls still needed timeouts. Retries needed backoff. Concurrency needed a cap.
Otherwise, a slow provider could leave too much work in flight, and repeatedly retrying a struggling service could make the situation worse.
Learning WebFlux and R2DBC meant learning to reason about the whole pipeline: where work started, where errors travelled, and what state should remain after a failure.
Retries made consistency more interesting
An event-driven system needs an answer to a fairly uncomfortable question:
What happens when the same work runs again?
I implemented idempotent persistence logic and an unprocessed-event runner to support reprocessing failed events. Database transactions helped roll back related changes when processing failed.
The aim was to make recovery possible without corrupting the application’s state.
Working on this made me pay much closer attention to partial progress. An operation could get through several steps before failing. Reprocessing it meant thinking about which changes had already happened and which ones still needed to happen.
There are also limits to what a local database transaction can protect. It can roll back database changes; it cannot undo a request already received by an external provider. That boundary is something I would pay particular attention to when strengthening the system further.
These were the parts of the project that made the architecture feel real to me. A diagram gives every operation a tidy arrow. Implementing it means deciding what happens when execution stops halfway along one.
Making the results inspectable
I also built lightweight views for checking the results.
The /analysis/dashboard endpoint brought together the latest bank-statement analysis and credit-bureau outcomes for each loan. It showed the loan and client information, the state of the checks, and links to successful results.
There were individual views for bank-statement and credit-bureau responses, plus an endpoint to stream the bank-statement analysis PDF report.
The UI was deliberately simple. It gave someone a way to inspect what the service had done without reconstructing the entire sequence from logs.
It was useful during development too. Having a visible result made it easier to follow a loan through the system and see where things stood.
See it in action
Here’s my project demo, starting at 22:58.
Debugging was a huge part of the learning
Debugging was one of the most valuable skills I developed through this experience.
I had to get comfortable following execution through an unfamiliar codebase, inspecting state, and working out where the behaviour diverged from what I expected.
Finding the issue mattered as much as writing the fix.
Sometimes that meant understanding a financial workflow I hadn’t encountered before. Sometimes it meant following the same action across the frontend and backend. During the project, it also meant tracing events, document handling, database changes, and external calls.
I got better at narrowing down a problem instead of treating the entire application as one big thing that wasn’t working.
That ability has stayed with me more than any particular framework API.
What I finished, and what I’d build next
By the end of the project, the service covered event ingestion, document handling, readiness checks, external integrations, persisted results, and operator views.
I added unit tests around the handlers, aggregator, dispatcher, converters, service statuses, and provider mappings.
There was still more to do. My final report called out integration tests for Kafka and PostgreSQL, additional providers, composite risk scoring, and further work on multi-tenancy and secret management. Production circuit breakers, metrics, and tracing were also follow-ups.
I think it’s useful to be clear about that scope. The project built the workflow for coordinating checks and inspecting their results. A complete, explainable composite risk score was still future work.
What I wanted from GSoC, and what I got
I went into GSoC wanting to upskill, with a fairly specific idea of what that meant: fintech, Java, and a microservice that talked to other systems.
I got that experience. I also got much better at the work around the code: asking questions, identifying issues, explaining an approach, and using feedback.
It was hands down one of my best learning experiences.
A big part of it started before selection, when I was still struggling with setup, reporting bugs, and trying to understand the application. Staying with it long enough made the unfamiliar parts less intimidating and gave me room to contribute.
And yes, after spending about a month getting the application running, I was quite happy to finally get to build something. XD