Skip to content

Credit & obligations platform

ProCrédito v2

Distributed credit platform: Laravel microservices behind an API gateway, Redis Streams, and Python workers for high-volume obligation processing.

Head of Software R&D · Architecture · Soga SAS · 2019 — Present

LaravelPHPPythonJSON:APIRedis StreamsMySQLNext.jsDockerKubernetesNginxHelm
Shape
Gateway + Laravel services + Python workers
Async backbone
Redis Streams
Operational store
MySQL per client, per service

Overview

ProCrédito v2 is a credit and obligations platform rebuilt as a set of independently deployable services — not a second version of a monolith with new folders.

Laravel microservices expose JSON:API over hexagonal / DDD slices (security, customers, obligations, settings, third parties). An API gateway authenticates and routes. Next.js workspaces consume those contracts.

Bulk obligation work does not live in HTTP. Python workers consume Redis Streams — ingest, validate, write — while Laravel remains the owner of the obligation aggregate and the domain write.

Problem

Credit operations ingest large obligation files, validate them against business rules, and persist them without blocking the interactive product. A single Laravel request that parses, validates, and writes that volume couples the user-facing API to a batch job.

The domain is also split: identity, customers, obligations, and third-party bureaus do not fail or release together. Sharing Eloquent models across those concerns produced a distributed monolith.

The first exchange capability is not “an importer.” It is a platform for exchanging obligations: contracts, idempotency, and workers that can be replaced without rewriting the aggregate.

Constraints

Internal Python workers have no HTTP API. They read streams and write through defined contracts. Cross-service communication is events (Redis) or the gateway — never a shared database session.

Expected denials are PolicyDecision + ErrorCode, not exceptions used as business branches. Field names, stream names, and error codes live in contracts — not as magic strings in use cases.

MySQL is the operational source of truth, with one database per client and per microservice (ms_*), so tenant isolation is a data-ownership rule, not a WHERE clause.

Architecture

The edge is coarse: the gateway terminates auth and routes to Laravel services. Business rules stay behind that edge. New Laravel features land in Application/Slices/{FeatureName} — Command, UseCase, Result, thin HTTP adapter.

Python obligation logic lives in domain/ and processors/, wired from consumers/runtime.py. The producer emits facts onto Redis Streams; workers react. The producer does not wait for every consumer.

Infrastructure (Eloquent, Redis, HTTP) stays in adapters. The domain does not import Laravel. That is how a worker and a controller can share a use case without sharing a framework.

Architecture

ProCrédito v2 — gateway, services, streams

Next.js apps

Obligation files

API Gateway

Auth, route, limits

Security

Customers

Obligations

Third parties

Redis Streams

Facts, not RPCs

Ingestor

Validator

Writer

MySQL

Per client · per service

As designed in the v2 platform: gateway at the edge, Laravel owns the aggregate, Python workers consume Redis Streams. MySQL is the operational store, isolated per client and service.

Key decisions

Personal experience

Laravel owns the aggregate; Python owns the pipeline

Obligation rules and the write path stay in Laravel. Python workers transform files and streams. That split keeps high-throughput processing off the request path without turning workers into a second domain.

Personal experience

MySQL as operational source of truth, isolated per client

Each microservice owns its schema. Tenant isolation is a database per client, not a shared catalog with a client_id column as an afterthought. Oracle was considered and rejected for this operational plane.

Personal experience

Gateway routes; it does not calculate credit

Authentication, request IDs, and routing belong at the edge. Obligation policy does not. A gateway that reimplements domain rules becomes a second, untested domain.

Personal experience

Contracts instead of shared Eloquent

Services integrate through versioned payloads and stream names. Sharing models across repositories looks convenient and produces a release train that no team can exit.

Trade-offs

Gained

Interactive APIs stay responsive while obligation files move through an independently scalable worker fleet. Services can release without a shared lock on one schema.

Sacrificed

Operational complexity: stream semantics, idempotency, and the discipline to keep HTTP off internal workers. A single stack trace no longer explains an end-to-end obligation.

Personal experience

Technologies

LaravelPHPPythonJSON:APIRedis StreamsMySQLNext.jsDockerKubernetesNginxHelm

Results

The v2 platform is the running product architecture: gateway, hexagonal Laravel services, Redis Streams, and Python workers — not a slide deck beside a monolith.

The first exchange capability replaced a file-importer mindset with an obligation pipeline (ingest → validate → write) that can evolve without rewriting the aggregate.

As of late 2026 the product was in UAT. Throughput figures are not published here.

Lessons learned

A worker that “just writes the database” will become the domain if nobody owns the aggregate. The split only holds if Laravel remains the source of truth for obligation state.

Event-driven design is a good trade when the producer should not know its consumers. It is a bad trade when the user-facing action is a lie until the side effect completes.

Architecture packs beat README folklore. When the rule is “slices, ports, Redis Streams, no HTTP on workers,” new work has a place to land.