Back to projects
In development

Payment Service Multi-Provider

Payment platform in Spring Boot with a gateway-agnostic architecture: the core for charging, subscriptions, checkout, credits and webhooks is provider-independent, with Asaas as the first real adapter and architectural support for new gateways without rewriting business rules.

JavaSpring BootMySQLDockerAWSReactTerraformSwagger

The Problem

Payment systems are usually born coupled to the first integrated provider. That spreads gateway-specific rules across the domain, makes testing harder, blocks future migrations and turns webhooks, errors, statuses and charging flows into something hard to maintain as the product grows.

The Solution

  • Modular architecture by business capability, with each domain isolated in its own module and explicit boundaries
  • Application core protected by ports and adapters, isolating business rules from payment-provider details
  • Asaas implemented as the first concrete adapter, without turning the domain into an extension of the provider's API
  • External webhooks translated into normalized internal flows, with idempotency, traceability and tolerance to redeliveries
  • Critical flows designed to avoid long transactions involving external gateway calls
  • Standardized error contracts with ProblemDetail and a clear separation between API, application, domain and infra

Architecture

  • Modular Monolith

    Java 21 / Spring Boot / Maven

    The system is organized by business modules, not by generic global layers. Each module concentrates its own API, application, domain, infrastructure, tests and contracts.

  • Core

    Ports & Adapters

    Use cases depend on outbound ports per boundary — gateway, repository, customer, subscription, notification and invoicing — keeping external details out of the core rules.

  • Asaas Adapter

    Asaas API

    The platform's first real provider, responsible for charges, subscriptions, Pix, boleto, card, tokenization, webhooks, customers and tax integrations.

  • Persistence

    MySQL / Flyway

    Relational model versioned by migrations, with attention to idempotency, processing history, financial statuses and operational traceability.

  • Events

    Outbox / Messaging / SQS

    Asynchronous flows are used for side effects and internal/external integration, reducing coupling between modules and avoiding fragile synchronous chains.

  • Observability

    OpenTelemetry, metrics and structured logs

    Critical operations are traced with a correlationId, per-flow metrics and logs useful for diagnosing partial failures, duplicate webhooks and gateway inconsistencies.

Services Used

  • Asaas
  • Amazon SQS
  • Amazon S3

Testing

  • JUnit 5
  • ArchUnit
  • Testcontainers

Challenges & Decisions

How do you keep the system independent from the gateway without falling into overly generic abstractions?

The approach was to design ports around the domain's real needs instead of merely mirroring the provider's API. The Asaas adapter translates external commands and responses into more stable internal objects.

How do you handle webhooks delivered multiple times or out of order?

Incoming events go through idempotency control, validation and normalization before changing internal state. The system treats redeliveries as expected behavior, not as an exception.

How do you avoid inconsistency between the local database and the external gateway?

Critical flows avoid keeping a database transaction open during external calls. Local persistence, status synchronization and failure handling are separated to improve traceability and recovery.

How do you evolve subscription, charging, credits, coupons and invoicing without building one giant, coupled service?

Each capability is isolated in its own module with explicit boundaries. Synchronous communication happens via ports when an immediate response is needed; side effects can be handled by events or asynchronous processing.

How do you prepare the system for new gateways without implementing them all up front?

The project was born gateway-agnostic by architecture, but with incremental delivery. Asaas is the first real provider; new providers come in as adapters when there is concrete demand.

Results

  • Financial core decoupled from the initial payment provider
  • Asaas integrated as a real adapter without contaminating the domain and use cases with external DTOs
  • Subscription, charging, checkout, credits and webhook flows organized by business modules
  • Safer processing against redeliveries, retries and partial failures
  • Foundation ready for multiple gateways without rewriting core rules
  • Testable, documented architecture suited to incremental evolution

Screenshots

Screenshot 1
Screenshot 2
Screenshot 3
Screenshot 4

Lessons Learned

  • Being gateway-agnostic doesn't mean implementing several providers up front; it means preventing the first provider from becoming the center of the architecture
  • Ports should represent real system boundaries, not decorative interfaces created by default
  • Webhooks must be treated as an asynchronous source that can be duplicated and eventually arrive out of order
  • Idempotency in payments is not a technical detail, it's a fundamental architectural rule
  • A modular monolith is a more pragmatic choice than microservices when the team needs to evolve fast while keeping transactional consistency and low operational complexity
  • Separating API, application, domain and infra prevents external DTOs, JPA entities and gateway contracts from leaking into the system core