skip to content
ainoya.dev

Deep Dive into maybe-finance/maybe

/ 4 min read

Introduction

A startup that was developing a personal asset management app recently ceased operations. However, in a commendable move, they have open-sourced their service on GitHub. You can find the repository at maybe-finance/maybe. From the README, it’s apparent that they invested over $1,000,000 in the development of this app. It’s quite rare to see such a large-scale, production-grade application code released to the public. With an interest in studying modern application development techniques, I delved into the code and structure of maybe-finance/maybe.

Overview of the Structure

According to their Monorepo File Structure Overview, the backend and frontend are both written in TypeScript. The repository adopts a monorepo structure managed by Nx, known for its rich plugin system and various features. For monorepo projects using Next.js, considering turborepo might be beneficial, especially since it’s backed by Vercel.

As of commit 0575beb5138a3f7644a69b0d7a76fad96b1f8d84, the directory tree is as follows:

$ tree -d -L 2
.
├── apps
│   ├── client
│   ├── e2e
│   ├── server
│   └── workers
├── libs
│   ├── client
│   ├── design-system
│   ├── server
│   ├── shared
│   └── teller-api
├── prisma
│   └── migrations
└── tools
    ├── generators
    ├── pages
    ├── scripts
    └── test-data

The GitHub wiki includes a decision tree guide on where to place which type of code. Such visual guides are invaluable for developers joining a project with an extensive directory structure.

Decision Tree

Monorepo File Structure Overview · maybe-finance/maybe Wiki

Used Libraries

Frontend

Backend

  • Database: PostgreSQL
  • ORM: prisma, with shared data model definitions between client and server.
  • Web Application Framework: express. There are backend implementations in Next.js using prismaClient to directly access the DB, but most implementations rely on express.
  • Worker: Bull, a job queue library for Node.js using redis for job management. It’s used for asynchronous processing of communications with external services like Teller, enhancing system stability and UX. It also functions as a cron scheduler.
  • Necessary middleware configurations are available in the docker-compose.yml.

Software Design

Frontend

  • Utilizes Next.js’s Page Router mode, operating in SSR. A vercel.json suggests hosting on Vercel.
  • Components that can be separated are modularized and managed with Storybook. This approach clarifies semantics, as a large number of tailwind class names can become unclear.
  • Avoids complex state management frameworks like Redux or Jotai, instead using a combination of hooks and react-query for simple state management. This approach is sufficient until the complexity of state management becomes an issue.

Backend

  • Actively uses Constructor Injection for DI. For instance, the IQueueFactory interface, implemented by classes like InMemoryQueueFactory and BullQueueFactory, facilitates easy swapping of persistence layers to in-memory during testing and debugging. Interfaces and their implementations use the class syntax. This DI paradigm is familiar to those primarily involved in backend development. Although factory methods for structuring objects are also common in TypeScript, the choice between these two approaches can vary.

DI Using Class

interface KVStore {
    get(key: string): Promise<string | null>;
    set(key: string, value: string): Promise<void>;
}

class RedisKVStore implements KVStore {
    // Initialization of the Redis client, etc.

    async get(key: string): Promise<string | null> {
        // Logic to retrieve the value from Redis
    }

    async set(key: string, value: string): Promise<void> {
        // Logic to set the value in Redis
    }
}

DI Using Factory Method

type KVStore = {
    get: (key: string) => Promise<string | null>;
    set: (key: string, value: string) => Promise<void>;
};

function createRedisKVStore(): KVStore {
    return {
        async get(key: string): Promise<string | null> {
            // Logic to retrieve the value from Redis
        },
        async set(key: string, value: string): Promise<void> {
            // Logic to set the value in Redis
        }
    };
}

Conclusion

  • The code is well-organized and consistently designed. The inclusion of generator code and docker-compose reflects a consideration for easy onboarding. Monorepo structures allow for a comprehensive overview with a single repository checkout. However, as the codebase expands, such oversight can become limited, making documents like the decision tree crucial for understanding.
  • I plan to continue exploring and writing about interesting OSS products, much like this analysis.

Update on 2024/02/07

A significant rewrite from React/Next.js to Ruby on Rails is underway at maybe-finance/maybe.