This blog post came out of the necessity of my own mistake. I have been working strictly with databases longer than I care to admit to. So in a project, we were making changes so that a specific application didn't have direct database access to a different application. Simple separation of concerns right, and microservice architecture. All that jazz right? So I brought in an HttpClient into the service layer, removing the repository. It seemed right at the time. Not sure why, several weeks later this pattern was copied by may other developers on the project, and just turn that project to no longer a clean looking platform of microservices. Then one day it hit me. The HTTP crud operations were doing the same thing as the database operations. And both should be treated as a datastore. Whether the data lives in a file of any structure, HTTP request, and or database. The access to that data should all live in the repository layer.
The code in this blog can be seen here. The git repository as of writing this post contains a library module with several sub-modules. The idea is that the libraries are then used by the applications that have yet to be written to further explain why repositories are for datastores not just databases.
Let's ignore the contact-core-test module for now. It contains a base test class. Each ContactRepository implementation test class can extend the base test class, and then only needs to write tests based on their implementation, but should pass all tests in the base class. I may turn that into a blog post of its own.
The contact-core module. This is a small simple module that contains the contact object. The repository interface for the contact object. The exception class for that object, and the validator class. The validator class just validates that the inputs specified in the methods of the interface adhere to the specifications of the javadocs of the interface.
package org.example;
/**
* A simple ContactRepository. Any implementation of this interface should
* leverage {@link ContactRepositoryValidator} to validate the overriding methods
* arguments. The implementations Tests classes should extend ContactRepositoryTests
*
* @see contact-core-test module
*/
public interface ContactRepository {
/**
* Search for the contact with the existing <code>id</code>
*
* @param id of the contact to be searched for
* @return <code>contact</code> maybe <code>null</code> if <code>contact</code>
* with the <code>id</code> doesn't exist
* @throws IllegalArgumentException if <code>id</code> is <code>null</code>
* @throws ContactException if an error has occurred
*/
Contact findContact(Integer id) throws IllegalArgumentException, ContactException;
/**
* Searching for <code>contact</code> that exactly matches the parameters
* passed into the method.
*
* @param firstName to be exactly searched with
* @param lastName to be exactly searched with
* @return a collection of <code>contact</code> that match the parameters
* @throws IllegalArgumentException if both parameters are <code>null</code>
* @throws ContactException if an error has occurred
*/
Iterable<Contact> findContacts(String firstName, String lastName)
throws IllegalArgumentException, ContactException;
/**
* Saves a new or updated contact information. If a <code>contact</code> is passed with the
* <code>id</code> is <code>null</code>. The contact will be treated as a new contact, and
* an <code>id</code> will be generated for it. If an <code>contact</code> has an existing
* <code>id</code> the <code>contact</code> with be updated.
*
* @param contact to be saved or updated
* @return saved <code>contact</code>
* @throws IllegalArgumentException if a <code>contact</code> with an existing <code>id</code>
* can not be found.
* @throws ContactException if an error has occurred
*/
Contact saveContact(Contact contact) throws IllegalArgumentException, ContactException;
}
The contact-file, contact-http, and contact-jdbc are all modules that implement the ContactRepository
. So an application then can bring in the specific technology implementation and build an application based on the interface.
A hypothetical scenario
As a product team, we are spinning up a contact service. We've requested a database, but that won't be ready for weeks, and the boss wants it done by next week. Well, we still can write that service. The rest application is written to the ContactRepository
interface. We bring in the contact-file dependency for the short term while we are waiting on our database to be complete. Once the database is complete we then can swap out the contact-file dependency for the jdbc one, only changing enough code to connect to the database. Then file contents are moved to the database after deployment. The boss shows off your work to another team that wants access to the contact info. Except they can't get past the firewall use the contact-jdbc implementation. Not that you wanted them to. So you create the HTTP implementation to your applications URL endpoints. Then they just need to write code to the interface you just did, but leverage HTTP to get the same data you got from the JDBC implementation. The good news is if your API change, you can control the HTTP implementation too, so you can save your customer the headache of handling the versioning, just give them the new HTTP jar with the versioned changes in it.
Top comments (1)
Nice! An informative post! π₯
PS, I noticed the image in the title is broken. π