Faster Feedback Using In-Memory Repositories
Delay DB persistence implementation when domain objects and relationships are in flux.
Originally published on . Last updated on .
One of my Twitch viewers was confused about the “Fake” and “In-Memory” Repositories I use, and wanted to know more about them. These are implementations of the Repository persistence pattern (from Domain-Driven Design), where we define an interface with methods to store and retrieve objects[1], but don’t specify how they’ll be stored. Here’s an example from my Ensemble registration system (aka MobReg):
public interface MemberRepository {
Member save(Member member);
Optional<Member> findByGithubUsername(String githubUsername);
Optional<Member> findById(MemberId memberId);
List<Member> findAll();
}
In-Memory repositories store objects in memory, instead of writing them to a database. They’re not suitable for production, but they are important for developing and testing your applications. If they’re used exclusively for unit testing, they’re called Fakes.
Faking It for Testing
Fakes are a type of Test Double used exclusively for tests that need to read and write to another object, like our Repository. We can isolate the code under test from the slowness and complexity of a real database repository by having the Fake store objects in memory.
I may write the Fake to only store a single object if that’s all the test needs (see this example from my Kid Money Manager project). Otherwise, I’ll use a Map
implementation to store multiple objects, as in this example. Since a Fake mimics the real thing, there’s no need to alter any tests (or code) when we switch to the real thing for production. However, there may be subtle differences between our Fake and a database implementation (often around transactions), so I’ll write (only a few) tests to ensure the code works with both.
I’ll go deeper into developing database Repositories using Fakes in a future article.[2]
Fast Feedback
The In-Memory Repository is intended to substitute for the database Repository at runtime, so the application can be used in a realistic way. I use an In-Memory Repository under two conditions:
-
When the application domain is changing a lot, and,
-
I’m not ready to commit to the database schema I need for production.
I can get quick feedback on changes to the Domain objects without spending the time to constantly update the database Repository code.
At a minimum, the in-memory implementation needs a way to find objects by their ID. Using a ConcurrentHashMap
makes this easy and protects against potential race conditions. If the Repository interface has other “finder” methods, e.g., findByFirstName()
, I will manually write those (as in this example). If I’m concerned the “finder” methods are complicated[3] and might not work correctly, I’ll write them via test-driven development (TDD).
Once I’m happy with the way the application works, I can switch to writing the database version. I’ll create migration scripts for the database and test them, usually using Test Containers. Finally, I’ll change the configuration to use these “real” Repository implementations, and manually check the application for any issues. I rarely find any at this point, so I then push to production. I’ve had zero bugs in production following this process, as part of my overall test-driven development process.
Should They Stay, or Go?
While developing MobReg, I found I switched to in-memory repositories so often that I created a specific configuration option to make it easier. Eventually, I’ll remove the in-memory versions once the object structure becomes stable (though that may not happen for a while!). For now, the quick feedback cycles are worth the minimal overhead of maintaining them.
If you want to watch me work on MobReg, or ask questions, join my Discord community.
Technically Repositories store and retrieve Aggregates, but that’s a whole separate article. Or three. ↩︎
Want to see this article sooner? Let me know on Twitter or Discord. ↩︎
Be careful, if you find yourself implementing complicated Fake Repositories, you might be doing too much, or you may need to limit testing that aspect of the Repository to a real database. ↩︎