The way we write code has changed over the years. In the 10+ years I’ve been writing code, I’ve followed different practices to “speed up development” or make code “more manageable” or “more readable”. It seems we as software engineers are always looking for ways to make our lives easier. We have to work in the code we write everyday, so why wouldn’t we want to make it easier, less frustrating, and less error-prone?
My style of writing code has been molded over the last couple of years. It’s based on domain driven design (DDD), with some shortcuts. It’s not perfect and may not work for everyone, but I’ve found it to better adhere to Clean Code principles, such as encapsulation, inheritance, single responsibility, and others.
Let’s show you what I mean with an example. We’re going to create a bank application that only retrieves account information and takes deposits (you don’t want to get your money out, do you?). Thinking of deposits, we can envision a few things.
- Controller endpoint to start a deposit and returns the new account balance
- Service with the method to implement the business logic to perform a deposit
- Repository to persist the deposit
I want to pause here to point out a few things. First, it’s important to note that the
AccountController relies on an interface, aptly named
AccountService . All domain-related classes (domain model objects, service interfaces, repository interfaces, etc.) are packaged together in a
domain package. So, with what we’ve defined so far, our
domain package would include
These three domain concepts live outside the implementation details of how this application uses them. To put another way, they have no idea they are part of responses in REST controllers or no notion of a specific database backing the repository.
The implementation details live in other packages, such as
repository. These packages hold the infrastructure details of how this particular application uses the domain concepts to make the banking application work, i.e. Spring Boot, JPA, REST, etc.
As an application grows, continue to organize packages with sub-packages, grouping related classes.
To know if you have the right level of separation between your infrastructure layers and your domain concepts, look at your imports. Your domain classes should only import from the
domain package. The domain should have no knowledge of the implementation details in which it is used. So, if it isn’t importing from say a controller or repository package, it’s safe to assume it’s not relying on infrastructure.
It’s also useful to keep infrastructure objects together. In the code above, we see a couple of objects referenced in the
AccountController that are used to receive and send information in REST calls. These are
DepositResponse , and
Since these are objects that should only be used in the case of sending and receiving data in the controller, it makes sense to have these objects live in the same package as the
AccountController , such as the
controller.account package. When structuring our code in this way, the
AccountController relies on either objects that come from the
domain package or objects that are directly next to itself in the code, in the
Why do this?
What does this get us? Our code is loosely coupled. The
AccountController does not rely on a specific, concrete service. We can implement the
AccountService in different ways without needing to change the controller. The different implementations can be defined in external configuration files and invoked for different active profiles.
It follows that when the
AccountService only relies on the domain
AccountRepository , the underlying data store that implements the
AccountRepository can be swapped out, without the
AccountService ever knowing. Our code is not only maintainable, but it is also easily modifiable, which is something we should all be able to get behind.
Back to the Code
Let’s dig into the details of some more implementation for our fake bank example.
These are the last couple of major classes in our example application. I want to point out the naming I chose for these classes. Notice I didn’t use the anti-pattern of
MyCoolBankAccountService doesn’t seem very realistic, but it is clear that this account service is for this particular bank that happens to be called “My Cool Bank”.
DatabaseAccountRepository leaves no doubt to engineers how this repository is backed. The specific database is an even deeper implementation detail that is left to Spring to handle, but we as actively developing engineers do know that we are working on the repository storing to a database and not one that’s going to Redis, S3, or any number of other data stores.
Why the “extra”
JpaAccountRepository ? I happened to choose to use JPA to interact with the database. I could have just as easily chosen (or could refactor in the future) to use something like JDBC Template instead.
I use these concepts everyday as I develop microservices. A nice side effect of writing code in this manner is as a service grows and we as a team decide it can be broken into its own service, any sub-package in the
domain package is ready to “lift-and-shift” into its own microservice, since everything in that package is only reliant on other classes in its package.
Following this pattern, paired with adhering to TDD, has allowed teams I’ve worked on to completely refactor applications from front to back without any risk. I hope this article helps you look at your package structure a little closer and decide what works best for you and your team. Writing code is an art form. Find your style and always look for ways to improve it.