Memoization
Memoization in Vla prevents duplicate database queries within a request, making your application faster without manual cache management.
The Problem
Section titled “The Problem”In a typical request, the same data is often fetched multiple times:
async function renderPage(userId: string) { const user = await db.users.find({ id: userId }) const posts = await db.posts.findMany({ authorId: userId })
// Later in the same request... const userAgain = await db.users.find({ id: userId }) // Duplicate query! const profile = await buildProfile(userAgain)
return { user, posts, profile }}This results in unnecessary database round trips.
The Solution
Section titled “The Solution”Vla’s memoization automatically caches method results per request:
class UserRepo extends Vla.Repo { db = this.inject(Database)
findById = this.memo((id: string) => { return this.db.users.find({ id }) })}
// In your serviceclass UserService extends Vla.Service { repo = this.inject(UserRepo)
async getUser(id: string) { // First call: executes the query const user = await this.repo.findById(id)
// Second call: returns cached result const userAgain = await this.repo.findById(id)
// No duplicate query! return user }}Creating Memoized Methods
Section titled “Creating Memoized Methods”Use this.memo() in your Repo classes:
class PostRepo extends Vla.Repo { db = this.inject(Database)
// Memoized by post ID findById = this.memo((id: string) => { return this.db.posts.find({ id }) })
// Memoized by author ID findByAuthor = this.memo((authorId: string) => { return this.db.posts.findMany({ authorId }) })
// Multiple parameters work too findByTag = this.memo((tag: string, limit: number) => { return this.db.posts.findMany({ tag, limit }) })}How It Works
Section titled “How It Works”Request Scoping
Section titled “Request Scoping”Memoization is scoped to the request (invoke scope). When a new request starts:
- A new scoped kernel is created
- A fresh memo cache is initialized
- All memoized methods start with an empty cache
// Request 1await GetUser.invoke('1') // Query executesawait GetUser.invoke('1') // Cache hit
// Request 2 (new scope)await GetUser.invoke('1') // Query executes again (new cache)Argument-Based Caching
Section titled “Argument-Based Caching”Results are cached based on method arguments:
class UserRepo extends Vla.Repo { findById = this.memo((id: string) => { return this.db.users.find({ id }) })}
// Different arguments = different cache entriesawait repo.findById('1') // Query executesawait repo.findById('2') // Query executesawait repo.findById('1') // Cache hit!Shared Across Instances
Section titled “Shared Across Instances”Because Repos use the invoke scope, the same instance is shared across your entire request. This means memoization works even when the repo is injected in multiple places:
class UserService extends Vla.Service { repo = this.inject(UserRepo)
async getProfile(id: string) { return this.repo.findById(id) // Query executes }}
class PostService extends Vla.Service { userRepo = this.inject(UserRepo) // Same instance!
async enrichPost(post: Post) { // Cache hit! No duplicate query const author = await this.userRepo.findById(post.authorId) return { ...post, author } }}Advanced Usage
Section titled “Advanced Usage”Fresh Calls
Section titled “Fresh Calls”Sometimes you need to bypass the cache and fetch fresh data:
class UserService extends Vla.Service { repo = this.inject(UserRepo)
async refreshUser(id: string) { // Skip cache and execute query return this.repo.findById.fresh(id) }}Cache Priming
Section titled “Cache Priming”Set cache values without executing the method:
class UserRepo extends Vla.Repo { findById = this.memo((id: string) => { return this.db.users.find({ id }) })
async create(data: UserData) { const user = await this.db.users.create({ data })
// Prime the cache with the new user this.findById.prime(user.id).value(user)
return user }}
// Later in the requestconst user = await repo.findById('new-id') // Cache hit!Preloading
Section titled “Preloading”Start a query in the background to warm the cache:
class UserService extends Vla.Service { repo = this.inject(UserRepo)
async getUserWithRelations(id: string) { // Start loading posts in the background this.repo.findPosts.preload(id)
// Do other work const user = await this.repo.findById(id) const settings = await this.repo.findSettings(id)
// Posts are likely cached now const posts = await this.repo.findPosts(id)
return { user, settings, posts } }}Busting Cache
Section titled “Busting Cache”Invalidate cache entries when data changes:
class UserRepo extends Vla.Repo { findById = this.memo((id: string) => { return this.db.users.find({ id }) })
async update(id: string, data: UserData) { const user = await this.db.users.update({ where: { id }, data })
// Bust the cache for this user this.findById.bust(id)
// Or bust all cached entries // this.findById.bustAll()
return user }}Memo API Reference
Section titled “Memo API Reference”Each memoized method has these utilities:
const repo = new UserRepo()
// Call normally (cached)const user = await repo.findById('1')
// Skip cache and execute freshconst fresh = await repo.findById.fresh('1')
// Prime the cacherepo.findById.prime('1').value(someUser)
// Preload in backgroundrepo.findById.preload('1')
// Bust cache for specific argsrepo.findById.bust('1')
// Bust all cached entriesrepo.findById.bustAll()Best Practices
Section titled “Best Practices”Do Memoize
Section titled “Do Memoize”- Database queries
- External API calls
- Expensive computations
- File system reads
Don’t Memoize
Section titled “Don’t Memoize”- Write operations (create, update, delete)
- Methods with side effects
- Non-deterministic functions
Example: Good vs Bad
Section titled “Example: Good vs Bad”class UserRepo extends Vla.Repo { // ✅ Good: Pure read operation findById = this.memo((id: string) => { return this.db.users.find({ id }) })
// ❌ Bad: Write operation create = this.memo(async (data: UserData) => { return this.db.users.create({ data }) })}Performance Impact
Section titled “Performance Impact”Memoization can dramatically reduce database load:
// Without memoization: 100 queriesfor (let i = 0; i < 100; i++) { const user = await db.users.find({ id: '1' })}
// With memoization: 1 queryfor (let i = 0; i < 100; i++) { const user = await repo.findById('1') // Only first call queries DB}In real applications, this translates to:
- Faster response times
- Lower database load
- Reduced API costs (for external services)
- Better scalability