DEV Community

Zied Hamdi
Zied Hamdi

Posted on • Updated on

Architecting a Pay-As-You-Go Service Library

Architecting Pay-As-You-Go: UserCredits' Adaptable Design

In the previous article, we introduced UserCredits, an open-source project designed to simplify the implementation of pay-as-you-go features in your applications. We explored the core concepts of UserCredits, including offers, orders, user credits, and token timetables. Now, let's dive deeper into the project's architecture, highlighting key design choices that make it both flexible and easy to extend.

The Need for Flexibility

UserCredits aims to provide a flexible and technology-agnostic solution for managing pay-as-you-go features. To achieve this, we separate core business logic from specific database implementations. This separation allows us to support various databases without changing the fundamental logic of the application.

Meet the DAO Factory

At the heart of UserCredits' architecture is the DAO Factory (Data Access Object Factory). It abstracts the data access objects (DAOs) and their dependencies for various entities, such as offers, orders, token timetables, and user credits. By using this factory pattern, we decouple our business logic from the underlying database technology.

Here's a closer look at how it works:

import { IOfferDao, IOrderDao, ITokenTimetableDao, IUserCreditsDao } from "../db/dao";

export interface IDaoFactory<K> {
  getOfferDao(): IOfferDao<K>;
  getOrderDao(): IOrderDao<K>;
  getTokenTimetableDao(): ITokenTimetableDao<K>;
  getUserCreditsDao(): IUserCreditsDao<K>;
}
Enter fullscreen mode Exit fullscreen mode

The IDaoFactory interface defines methods for creating DAO instances for different entities. Each DAO is responsible for interacting with a specific database collection. This separation of concerns makes it easy to switch between database implementations, whether it's MongoDB, PostgreSQL, or another system.

DAOs: Abstracting Database Operations

DAOs (Data Access Objects) serve as the bridge between our business logic and the database. These DAOs provide methods for CRUD (Create, Read, Update, Delete) operations on database entities by default, but can be extended to more specific jobs.

For example, here's a simplified version of the IOfferDao interface:

export interface IOfferDao<K> extends BaseDao<K> {
  createSubOffer(offer: IOffer<K>): Promise<IOffer<K>>;
  findSubOffers(offerId: K): Promise<IOffer<K> | null>;
  // ... other CRUD methods
}
Enter fullscreen mode Exit fullscreen mode

With these DAO interfaces in place, we can implement concrete DAO classes for different database technologies. The key is that the business logic doesn't change; only the underlying DAO implementations do.

Flexible Service Implementation

UserCredits also provides a flexible IService implementation, which serves as a facade for our pay-as-you-go features. This service abstracts complex operations, such as creating orders, executing payments, and checking remaining tokens. By keeping this logic in the service layer, we ensure consistency and simplify the testing process.

export interface IService<K> {
  createOrder(offerId: K, userId: K): Promise<IOrder<K>>;
  execute(order: IOrder<K>): Promise<IUserCredits<K>>;
  orderStatusChanged(orderId: K, status: "pending" | "paid" | "refused"): Promise<IOrder<K>>;
  remainingTokens(userId: K): Promise<IUserCredits<K>>;
}
Enter fullscreen mode Exit fullscreen mode

The IService interface defines methods for performing high-level operations related to pay-as-you-go functionality. This separation of concerns allows us to write tests for our service without worrying about the underlying database implementation.

The Power of Inversion of Control (IoC)

To bring everything together, UserCredits uses Inversion of Control (IoC) through the Awilix library. IoC allows us to manage dependencies and inject the appropriate DAOs into our service and other components.

Here's how it works in practice:

import { asFunction, createContainer } from "awilix";
import { DaoFactory } from "./dao/DaoFactory";

const container = createContainer();

// Register the DAO Factory as a transient value
container.register({
  daoFactory: asFunction(() => {
    const dbFactory = new DaoFactory();
    return dbFactory.init(); // Initialize and configure the database connections
  }),
});

export default container;
Enter fullscreen mode Exit fullscreen mode

With IoC, we can easily swap out different implementations of the DAO Factory based on our database needs. This flexibility is essential for scaling and adapting UserCredits to various projects and technologies.

Conclusion

UserCredits' architecture is designed with flexibility and maintainability in mind. By separating core business logic, abstracting database operations with DAOs, and leveraging Inversion of Control, we've created a powerful framework for managing pay-as-you-go features. This architecture allows us to support different databases, write efficient tests, and adapt to specific project requirements seamlessly.

In the following articles, we'll explore concrete payment processes, explain our tests, and start talking about UX use cases. Stay tuned for more insights into the evolution of this open-source project.


This article provides a more comprehensive overview of UserCredits' architecture, focusing on the DAO Factory, IService implementation, and IoC. It aims to give readers a deeper understanding of how UserCredits achieves flexibility and adaptability in managing pay-as-you-go features.

Top comments (0)