A REST API for controlling geographic concentration risk in loan portfolios.
Loan Risk is a backend application designed to evaluate and control new loans against geographic concentration limits defined for a loan portfolio.
The project focuses on backend engineering practices such as Domain-Driven Design, Service Objects, Transactional Consistency, Configurable Business Rules, Financial Data Modeling, and Automated Testing.
- ๐ฐ Create and manage loans records
- ๐บ๏ธ Calculate geographic concentration across the loan portfolio
โ ๏ธ Reject loans that would exceed concentration limits- ๐ Expose current portfolio concentration through the API
- โ๏ธ Store concentration rules as database records
- ๐ต Precise monetary calculations using PostgreSQL decimal
- ๐ Use database transactions for financial operations
- ๐งช Automated tests with RSpec
- ๐๏ธ Service Object architecture for business rules
- ๐ณ Docker support
- Ruby
- Rails
- Rails API-only
- Active Record
- PostgreSQL
- Service Objects
- Domain-driven design principles
- REST API
- RSpec
- Docker
Loan portfolios can become excessively concentrated in a specific geographic region.
For example, suppose a portfolio has a maximum concentration limit of 20% per state.
If the portfolio currently contains:
Total portfolio: R$ 100,000
Sรฃo Paulo (SP): R$ 15,000
Current concentration: 15%
A new loan of:
R$ 10,000 โ SP
would result in:
Projected SP exposure: R$ 25,000
Projected concentration: 25%
Allowed limit: 20%
The API therefore rejects the new loan because accepting it would violate the configured concentration limit.
This business rule is the core of the application.
When a loan is created, the system:
New Loan
โ
โผ
Calculate projected portfolio
โ
โผ
Calculate geographic concentration
โ
โผ
Compare against configured limit
โ
โโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
Within limit Exceeds limit
โ โ
โผ โผ
Approve Reject
โ
โผ
Persist loan
The concentration calculation considers only loans with:
status: "active"
Cancelled loans are excluded from the portfolio concentration calculation.
The application currently follows these business assumptions:
- The total portfolio value is the sum of loans with
status: "active". - Only loans with
activestatus are included in concentration calculations. - Cancelled loans are excluded from concentration calculations.
- A state-specific concentration rule overrides the default rule.
- A loan exactly at the configured concentration limit is considered approved.
The application uses a domain-oriented architecture with Service Objects to keep business rules separate from controllers and persistence concerns.
โโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP Client โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ Controller โ
โ โ
โ Handles HTTP/JSON โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ Service Object โ
โ โ
โ Business rules โ
โ Concentration logic โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ Models โ
โ โ
โ Validations |
โ Associations |
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ PostgreSQL |
| |
| Persistence โ
โโโโโโโโโโโโโโโโโโโโโโโโ
The main responsibility boundaries are:
- Controller โ HTTP requests and responses
- Service โ Business rules and risk calculation
- Model โ Data integrity and validations
- Database โ Persistent rules and loan data
This separation makes the business logic easier to test, maintain, and evolve independently from the HTTP layer.
The core business requirement is geographic concentration risk.
When a new loan is created, the application evaluates how the loan would affect the portfolio's concentration in the corresponding state.
The application supports a default concentration rule and more specific rules.
A more specific rule takes precedence over the default:
Specific state rule
โ
Default rule
This allows the risk policy to be configured without embedding every possible rule directly into the application code.
A key design decision in Loan Risk is to store concentration rules in the database instead of hardcoding them in Ruby.
For example:
scope_type: "state"
scope_value: "SP"
limit: 20%
Changing the limit from:
20% โ 30%
becomes a data operation rather than a code change.
This means that changing a business configuration does not require modifying the application code or deploying a new version.
The rule model also supports:
scope_typescope_value
This allows the system to evolve toward rules based on other dimensions, such as:
- Region
- Product
- Customer segment
- Portfolio
- Other business criteria
Monetary values are stored using:
decimal(15, 2)rather than floating-point types.
This is intentional because floating-point arithmetic can introduce binary rounding errors that are inappropriate for financial calculations.
For example, a monetary value such as:
10000.00
should be represented precisely rather than relying on floating-point arithmetic.
Loan creation and concentration validation are financial operations where consistency is critical. The application is designed around relational database guarantees and ACID transactions provided by PostgreSQL.
The goal is to ensure that the concentration check and the resulting persistence are handled consistently rather than allowing the portfolio state to become invalid between operations.
POST /loans
Content-Type: application/json
{
"amount": 10000,
"state_code": "SP"
}If the new loan respects the applicable concentration limit, the API returns 201 Created.
If the loan would exceed the limit, the API returns 422 Unprocessable Entity with an explanatory error.
Example:
{
"error": "Concentration limit exceeded for SP"
}Request:
{
"amount": 10000,
"state_code": "SP"
}HTTP/1.1 201 Created{
"id": 1,
"amount": 10000.0,
"state_code": "SP",
"status": "active",
"created_at": "2024-01-01T10:00:00.000Z"
}HTTP/1.1 422 Unprocessable Entity{
"error": "Concentration limit exceeded for SP: 25.0% projected, allowed limit is 20.0%"
}GET /loansGET /loans/:idGET /loans/concentrationExample response:
{
"portfolio_total": 100000.0,
"states": [
{
"state": "SP",
"amount": 60000.0,
"concentration": 60.0,
"limit": 20.0,
"within_limit": false
}
]
}The project uses RSpec to test the application's behavior at different layers.
Run the complete test suite:
bundle exec rspecRun model specs:
bundle exec rspec spec/modelsRun service specs:
bundle exec rspec spec/servicesRun request specs:
bundle exec rspec spec/requestsThe test structure mirrors the application's architecture, making it possible to test:
Models
โ
Services
โ
HTTP Requests
This helps ensure that both individual business rules and complete API flows are covered.
Make sure you have installed:
- Ruby 3.x
- Rails 7.1
- PostgreSQL
- Bundler
Or run with Docker in a containerized environment.
git clone https://github.com/digidweb/loan-risk.git
cd loan-riskbundle installrails db:createrails db:migraterails db:seedThe seed data creates the initial concentration rules.
rails serverThe API will be available at:
http://localhost:3000
The repository also includes a Dockerfile for containerized development.
docker build -t loan-risk .docker run -p 3000:3000 loan-riskFor a complete production-like environment, PostgreSQL should be provided as a separate service/container and configured through environment variables.
The main purpose of this project is to demonstrate how a relatively simple business requirement can be translated into a maintainable backend architecture.
The application intentionally separates:
HTTP concerns
โ
Business rules
โ
Domain entities
โ
Database persistence
This makes it possible to evolve the business rules without coupling them directly to the API controllers or database implementation.
If continuing the project, the following improvements would be:
- Add pagination to GET /loans
- Add API authentication and authorization
- Add rate limiting
- Add domain events for integrations
- Add caching for portfolio concentration calculations
- Add an administrative interface for managing risk rules
- Add GitHub Actions for automated tests and code quality checks
- Add API documentation with OpenAPI/Swagger
Loan Risk is part of a portfolio focused on Ruby on Rails backend and full-stack development.
The project demonstrates practical experience with:
Ruby on Rails ยท REST APIs ยท PostgreSQL ยท Active Record ยท Service Objects ยท Domain Modeling ยท RSpec ยท Docker
More importantly, it demonstrates the ability to translate a business requirement into a maintainable backend architecture:
Business requirement
โ
Domain model
โ
Business rules
โ
Service Object
โ
Database / Persistence
โ
API response
โ
Automated tests
The project goes beyond basic CRUD by focusing on business rules, data integrity, separation of responsibilities, and testable application architecture.