This project implements a payment gateway using .NET 7, PostgreSQL, and Dapper. The main functionalities of this gateway are:
- Register merchants and generate secret_keys to use on authentication
- Validate, process, and store credit card payments
- Retrieve payments by a merchant or by id
- Clone the project and navigate to the project folder
git clone https://github.com/fsnader/CheckoutGateway.git
cd CheckoutGateway/src/CheckoutGateway/CheckoutGateway- Navigate to the project folder
cd - Run docker-compose to start the PostgreSQL server and create the database schema
docker-compose up -dOn the first container run, the database schema will be created automatically. If you need to make any changes to the schema or initialize the database in any other way, you can modify the initialize.sql script located in the .postgres directory.
- To start the application, you will need to start the project through your IDE (Visual Studio, Rider) or run the dotnet command:
dotnet run- After that, the running address will be printed in your terminal, and you will be able to access the url to see the Swagger OpenAPI documentation.
Authentication is required for all endpoints except for the POST /api/auth/signup and POST /api/auth/login endpoints. Users must include an Authorization header with a JWT token to access protected endpoints.
POST /api/auth/signup: Allows merchants to sign up and create a new account. This endpoint requires a merchant name and a client_id, and will generate the secret_id that is required to log in.POST /api/auth/login: Receives a client_id and a secret_id, and returns a token that can be used to access the payments endpoints.
To use the received token on Swagger, it's necessary to copy the received token, click on the "Authorize" button, and add the token in the input, in the format "Bearer ${token}":
The application consists of three main API endpoints:
POST /api/payments: Receives a payment request (containing the amount, currency, and credit card details), validates it, sends it to the payment gateway, and keeps the database updated accordingly.GET /api/payments: List all payments for a specific merchant.GET /api/payments/{id}: Retrieves a specific payment by its id. It only returns the payment if it's from the merchant that is performing the request and masks sensitive information.
This project tries to follow some Clean Architecture patterns to keep the domain and the business logic isolated from implementation details, making it technology agnostic and easily testable.
In a bigger project, the application layers would be kept in different projects (and .dlls), but as this system has only a few use cases, I intentionally decided to organize it using folders:
This is the core domain layer, where entities and abstraction interfaces are defined. This layer doesn't have any dependency on a specific technology and contains the most stable components.
This is the layer that keeps application use cases. Each use case represents a business case and contains a single responsibility. The only dependency of this layer is the domain layer.
- Merchants:
- CreateMerchant: Creates a merchant, generating a client_id and a secret_id to be used for login
- Payments:
- CreatePayment: Creates a payment on the database, sends it to the bank gateway, and processes the response.
- GetPaymentById: Gets a payment for a provided Id and MerchantId
- GetPaymentsList: Lists all the payments for a specific merchant OutputPots: Abstractions that represent the result of a use case. The main class here is the UseCaseResult, which is a generic envelope that encapsulates the response of a UseCase to simplify success and error. Validators: FluentValidation input validators
This is the layer that keeps the implementation details of the domain abstractions. In this application, most of this is related to data access (repositories) and external integrations (gateways).
Includes the BankGateway implementation. This mock class emulates the connection between the gateway and the Bank and returns random results. For a real project, it probably would implement a service integration through queues, REST APIs, GRPc, or any other communication protocol.
Includes abstractions and classes related to Postgres data access.
For this project, Dapper is being used to access the database. This MicroORM increases the initial effort of doing CRUD operations, but gives much more control and safety for the developer, especially for more complex queries.
Includes the repositories implementations to access Merchant and Payment entities.
This is the layer that keeps the implementation details of the presentation layer (API Controllers) of our app, and components that only make sense on this layer (Authentication and Middlewares).
This layer also includes DTOs which are the API contracts. These contracts usually differ from the domain because they are use-case specific and hide sensitive domain information from the end user.
The CheckoutGateway.UnitTests project contains a suite of unit tests for testing the individual components of the API. These tests are designed to test the logic of the code in isolation, without relying on external dependencies. The tests are written using xUnit and can be run from Visual Studio or using the command line.
- List payments endpoint
- Merchant signup and authentication
- Unit tests
To do a real cloud deployment, we could use the following services:
- Database: Use Amazon RDS to deploy a managed PostgreSQL instance
- Application: Use Amazon EKS to deploy a managed Kubernetes cluster and deploy the application on it in a scalable way
Some other aspects could be considered for a real implementation of a payment gateway, and that hadn't been considered on this project to keep things simple.
The secret ids should be saved in the database using some sort of hashing and salting. This would ensure that even if the database is exploited, login information wouldn't be exposed.
An import testing strategy for this project would be to test the whole API flow, to ensure that all infrastructure implementations (databases, APIs, gateways) are working properly.
To avoid duplicated payments, it would be interesting to include an idempotency header on the payment creation request. This header would be checked to block any duplicated processing.
In an HTTP integration with the bank, we could include Polly to implement some retry and circuit breaker strategies. This would ensure that temporary network or service problems would affect customers less.
It would be interesting to keep some sort of event tracking table containing all the payment status updates. This would give more visibility of aspects like when the payment had been created when it got accepted, when it got finished, etc...
The current implementation assumes that the CreatePayment request will return the final result of the payment synchronously, like the following sequence diagram:

Instead of following this synchronous flow, the gateway processing could be done asynchronously:
- Our gateway sends a request to the bank with the payment information
- The bank does a fail-fast validation to ensure the input is valid. It is, the bank returns a 202 Accepted result confirming that the payment will be processed.
- When the bank updates the transaction (accepting, rejecting, etc) it notifies our gateway in a webhook
- This webhook then updates our database accordingly and notifies subscribed customers
This option has some advantages:
- The customer doesn't have to wait for the whole payment processing on the same HTTP request, which improves the user experience
- In case of any failures on the gateway or bank processing, we can retry a few times before rejecting the transaction, which improves the reliability of our system
But it also brings some disadvantages:
- The architecture is much more complex and has a lot of moving parts
- The development, monitoring, and maintenance efforts are higher
- Handling real-time notifications can bring other technical and UX challenges
Because of that, it would be important to evaluate the trade-offs before following this architecture.










