Over 2,000 mentors available, including leaders at Amazon, Airbnb, Netflix, and more. Check it out
Published

AWS QLDB with Node and NestJS

QLDB is an immutable blockchain-based ledger database from AWS. In this post, we'll review how to use it with NestJS and NodeJS and key benefits of using QLDB.
Kerry Ritter

Senior Software Engineer, Microsoft

I was introduced to AWS's Quantum Ledger Database in 2021 at Lumeris as we looked for a way to manage data access logs in a simple, scalable, and cost-effective manner. This log datastore needed to be immutable, queryable, and able to handle hundreds of thousands of writes per day. We found an excellent solution in QLDB. After using it here, I gave it a shot as my database for a side-project. During these two experiences, I've found some huge wins and a couple of roadblocks, so I'd like to share some of my findings. Expect more to come - I am not done with QLDB! :)

Using QLDB with Node & Nest

My colleague Ben Main and I published a library nest-qldb which made working with QLDB insanely easy. With an out-of-the-box repository pattern and a simple Dapper-esque (for those familiar with the popular .NET ORM) query service, it only takes a couple of decorators to get up-and-running in our favorite NodeJS framework, NestJS. Below is an excerpt of our e2e tests that shows how to use the @QldbTable() decorator:

@QldbTable({
  tableName: 'app_users',
  tableIndexes: ['dob', 'sex'],
})
class User {
  dob: Date;
  name: string;
  gender: string;
  sex: 'M' | 'F';
  luckyNumber: number;
  groups: { name: string }[] = [];

  constructor(partial: Partial<User>) {
    Object.assign(this, partial);
  }
}

@Injectable()
class UserService {
  constructor(
    @InjectRepository(User) readonly usersRepository: Repository<User>, // auto-generated repository
    private readonly queryService: QldbQueryService, // simple query service
  ) {}

  async create(data: User): Promise<User & { id: string }> {
    const result = await this.queryService.querySingle<{ documentId: string }>(
      `INSERT INTO app_users ?`,
      [data],
    );

    return {
      ...data,
      id: result?.documentId,
    };
  }

  /**
   * Retrieves a record based on the QLDB id.
   * @param id The QLDB ID of the object.
   */

  async retrieve(id: string): Promise<User & { id: string }> {
    return await this.queryService.querySingle<User & { id: string }>(
      [
        `SELECT id, u.*`,
        `FROM app_users AS u`,
        `BY id WHERE id = ?`,
      ].join(' '),
      id,
    );
  }
}

@Module({
  imports: [
    NestQldbModule.forRoot({
      qldbDriver: new QldbDriver('test-ledger'),
      createTablesAndIndexes: false,
      tables: [User],
    }),
  ],
  providers: [UserService],
  exports: [UserService],
})
class TestRootModule {}

Wins

Document Design with a SQL-like Query Layer

For the most part, QLDB is a document database - nested documents, complex data designs, and all that good stuff. However, your access layer is a SQL-like language, PartiQL. While it may not be as fully fleshed out as the query languages of the world, it can do all the typical things you'd be needing to do, including JOINs (be careful!) and nested document querying.

PartiQL felt very natural. Nested queries made sense, but the SQL-like language design made me feel right at home. I was very happy to work with PartiQL and didn't feel like I was fighting the language or digging through documentation to figure things out.

Immutability and Historical Tracking

This is one of the core tenant of QLDB: data immutability out of the box with a historical context. For our access log purposes, this is exactly what we needed - along with the data integrity guarentees that QLDB provides.

Affordability*

QLDB is SO COST-EFFECTIVE! For context, we logged a million access logs and spent something like $0.50. It is also pay-as-you-go, which makes it wonderful for side-projects or apps that don't need to do a ton of complex querying. You could host your queryable data for under a dime for a year!

HOWEVER, you need to be aware of your document design and of your queries. Check out the roadblock section below on document design, but the synopsis is that you can shoot yourself in the foot if you're not being careful with your queries and indexes.

No-Maintenance Infrastructure

If you've worked with AWS DocumentDB or serverless Aurora, you know there's a bit of work to do as far as infrastructure setup and management. With QLDB, there isn't ANY work here. Literally. Zero. It's astounding. Here's the whole CloudFormation definition to set up your ledger:

Resources:
  Qldb:
    Type: AWS::QLDB::Ledger
    Properties: 
      Name: !Ref QldbLedgerName
      DeletionProtection: true
      PermissionsMode: ALLOW_ALL

Five lines of YAML and you have a serverless, immutable, ledger database. Can't beat that.

Stream Events

One great feature we leveraged is having events streaming after writes to the ledger database. In our case, every time we log an access record, we trigger a Lambda event that sends the details to a third-party API. Very low-effort on our part, and very easy to integrate. Another great use of this would be to ship it to over to be surfaced through ElasticSearch for super-fast querying (see the roadblock section for more information).

Blockchain Technology

The buzzword (buzzphrase?) makes it cool by default. Sorry, it is what it is.

Roadblocks

Indexing

There are a few core issues with indexes that made me change my mind about using QLDB for LoudChorus, but from what I hear, there are some maaajor changes coming in the future and I'm very excited to see the capabilities. Here's the main issues I found:

  • QLDB only supports indexing top-level properties, which limits some of the nested document querying capabilities.
  • Indexes are only useful when doing equality queries. This makes searching QLDB less performant, which is why using stream events and ElasticSearch may be a preferable approach to querying QLDB directly.
  • Indexes can only be applied to new tables. This was problematic because it meant as I added new columns that needed indexes, I needed a migration strategy because I couldn't add the new indexes.

Close Attention to Document Design is Crucial

I played with a few designs for the LoudChorus database, particularly a relational approach and document design approach. The relational approach failed drastically - the performance was awful (as in, it couldn't complete the queries in 30 seconds), but the document design approach worked fantastically. While I expect the indexes in the relational approach to buy some performance gains, they definitely did not.

My qualm here is that, while you need to be smart with any data store, you need to be particularly careful with QLDB for a couple reasons:

  1. Not using indexes will crush your performance and the affordability. Your data needs to be structured so that it can be looked up with an equality operation.
  2. You can't add indexes after you create your table, which will create some extra legwork for migrations.

Affordability*

In playing with some load tests of a few thousand records with some LIKE queries, I ended up racking 3 million read IOs at the cost of about $0.50. Really, this is nothing - but this would come to about $180/year, which becomes less interesting.

This happened because I was doing some pretty gnarly querying - LIKE queries against multiple columns in nested documents, and querying non-indexes. I don't blame QLDB because I was going off the rails, but it's something you really need to be conscious of when you're designing your data store and building your queries.

Overall Impression

I really like QLDB and will definitely be using it on products now and in the future. For now, it won't be able to fully replace my Mongo Atlas data store because of the state of indexing, but I'm looking forward to the incoming features.

Give QLDB a shot. Try Nest and nest-qldb to get a quick start!

Find an expert mentor

Get the career advice you need to succeed. Find a mentor who can help you with your career goals, on the leading mentorship marketplace.