All Articles

Application Architecture - Correctly packaging classes, services, repositories and more

Multitude of gifts, nicely packaged together
Just like nicely packaged gifts, a nicely packaged application setup will bring you much joy over time...

Now that we have a new application to develop the backend, you may want to immediately jump into it. That makes sense, as adding new features is what development is all about, anyways, right?

That may be mostly true, but it always pays off to think first on how to define what goes where. Microservices are obviously a lot smaller than big, monolithic applications, but they still need to be neat, as even in small applications (that may grow over time also) chaos is never your friend.

In this article, I will go over DDD, domain driven design, and show how a potential layering of your application could look.

Domain Driven Design

DDD in short, or Domain Driven Design, is, as defined by Martin Fowler, “an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain”. In this concept, the structure and language of the code should be primarily focused on the business domain. It forces you to always think first of what the usage of the new code is, and to then explain it clearly. This means for example that the naming is always very obvious, and in sync with the language that business would use.

An important additional feature of DDD is that much of the logic is directly in the domain’s entities. For example, let’s say we have a system where we have users that can be active or not. You would have multiple options of creating that behaviour:

  1. Simply do user.setActive(true)
  2. Create a UserService with an activateUser(user) method, which would then set the user’s activity to active
  3. Create a method in the User entity called activate

You may look at these options, and say they are all equivalent. While that may be true, DDD would argue that option 3) is the cleanest way to perform that operation. The reason is that option 1) and 2) require a setter on that particular field. Option 1) would even potentially be called anywhere, and would be hard to track. Option 2) already shows a bit more thinking ahead, as you would realise something’s amiss if you suddenly need to inject that Service everywhere. But option 3) really shows that you have put some thought into the operation and that you’ve come to the conclusion that this method really is required! It’s not simply generating a bunch of setters because they’re convenient and can be used anywhere - you have actually decided this functionality is needed.

This goes hand in hand with the additional design that is required to do DDD correctly. There is a clear distinction between entities, services, and simple helpful objects. Entities are objects that are not defined by its attributes, but its identity. Value objects on the other hand are purely used for the attributes they contain, and should therefore always be immutable. This also means two value objects with the same value are always identical, whereas two entities with the same values are separate.

One more important point of DDD is the clear separation of business logic through bounded contexts. You cannot avoid it that multiple contexts may require to handle the same entity. However, they will usually require them for different reasons.

Bounded Contexts
Both contexts require customers and products, but for vastly different reasons.

As you can see in the picture above, both Sales and Support have knowledge of customers and products. However, they need this knowledge for different reasons. A Sales person might want to know their budget, their dreams, etc., in order to sell them the best product. Support staff on the other hand do not need to know this information, but rather the tickets that the customer has made, and how to best satisfy those needs. So each context needs different attributes, even though the identity of the customer is exactly the same.

There is, of course, much more to DDD than what I’ve presented here, but this article is more about hands-on implementation of DDD. Feel free to do your own research as well though.

Package Setup


The clear definition of boundaries and contexts is not only important for different contexts, but also within one single context. The typical separation of packages is already like so:

  • Repositories go into a repository package
  • Services go into a service package
  • Etc

Most projects are set up this way, but oftentimes developers do this just because that’s what they’ve always seen. The packages are therefore set up correctly, but the classes are afterwards still all over the place. Services are injected into Mappers, Mappers into services, until they reach a cyclical dependency, and the application doesn’t start anymore. No problem - inject the mapper into the service, and use @PostConstruct in the Mapper to set the Service. You get the picture - this is far from ideal!

A clean project setup can be seen as an onion, as represented in the picture below. Items from the center can migrate to outer packages, but never the other way around.

DDD onion
Both contexts require customers and products, but for vastly different reasons.

In essence, that means:

  • Controllers can contain Services and Repositories
  • Services can contain Repositories
  • Incoming DTOs do not get further than the Controller layer, where they are immediately mapped to Domain Objects
  • Outgoing DTOs should not be in the Services, but instead be mapped from Domain Objects as late as possible, i.e. in the gateways or connectors

You can already tell that this will mean quite some mapping. However, the advantages can quickly become very large. Any change in an API, be that one exposed by the application, or one that it consumes, would mean changes only in very few places (ideally, only in the Mappers and their tests).

In practise

Now that we’ve seen the theory behind it, let’s define some packages (I won’t mention the root):

  • rest.controllers - RestControllers
  • rest.extension - The extension methods for DTO to domain model conversions and back
  • rest.dto - The DTO models that the APIs will expose
  • - Services
  • domain.entities - Entities (that are stored in DBs)
  • domain.repositories - Repositories
  • gateways - Gateways/Connectors that make outgoing calls
  • security - Security related classes

Of course, in the future, we may need to adapt this structure. DDD is a living idea, and not a topic to do once and then cross off your list.

End-to-end functionality

Okay, now we’ll go ahead and actually start coding!

Let’s start with two different endpoints:

  1. Store a new account
  2. Retrieve that account

In theory, if we have enough knowledge, we should also apply TDD (Test Driven Design), and start creating our Unit tests first. I try to use this pattern as often as possible, but since I will probably add an article for BE testing and explain TDD there, I will only define the classes here.



The first class we’ll add will be the Account entity.

@Table(name = "account")
class Account(
    @Id val id: UUID = UUID.randomUUID(),
    @Column(name = "name", nullable = false) val name: String,
    @Column(name = "amount", nullable = false) val amount: Double,
    @Column(name = "currency", nullable = false) val currency: Currency,
    @Column(name = "description", nullable = false) val description: String,
    @Column(name = "added_on", nullable = false) val addedOn: LocalDate =

Those are, for now, the only fields we’ll need. This will be extended in the future, when we add users, historical snapshots, etc, but for now, this’ll do.

As you can see, we’ve not used the data class, which may sound counter-intuitive. I mean, there’s something called a data class, how can it not go into a database? Well, it’s generally discouraged to use data classes along with JPA. There are some reasons for this:

  • Data classes cannot be open, whereas JPA entities can form data hierarchies
  • JPA is not designed to work with data classes’ generated methods


The repository is fairly simple now:

interface AccountRepository : JpaRepository<Account, UUID> {

All the functions we will require for now will be straightforward, as we’ll start with findById, findAll and save, all of which are provided by the JpaRepository interface.


Now we need to think of the functionality we will require from the service. For now, we only want to store and retrieve new accounts, one by one. So why even go through a Service? The onion model has shown that we can simply inject the Repository into the Controller. However, things are rarely this simple. Here’s one example: since the findById returns a nullable account, we would need to have error handling in every place that this is to be used. If we abstract this into the Service though, this very simple function can take care of this already, and other places know they will get an actual, non-null account.

class AccountService @Inject constructor(private val accountRepository: AccountRepository) {

    fun getAccount(accountId: UUID): Account {
        return accountRepository.findById(accountId).orElseThrow{ AccountNotFoundException("Could not find an account with id $accountId.") }
    fun createAccount(account: Account): Account {

This will require the additional dependency in the pom.xml:


Also, we already have the first custom Exception. For this, I have added a new package domain.exceptions where I have made a Kotlinfile DomainExceptions.kt (I will add all simple Exceptions in the same file):

@ResponseStatus(code = HttpStatus.NOT_FOUND)
data class AccountNotFoundException(override val message: String) : RuntimeException()

Controller and Mapping

Okay, we’re almost done with our first features. All we need now are the endpoints to connect to from the consumer side, the DTOs, and the mappings from the DTOs. Let’s go bit by bit though, and start with the GETting of an account.

First things first, we do not want to expose the actual domain model to the outside world. Let’s start with a DTO for the account, which is indeed a data class, as it is only about the data that is contains, and not its identity:

data class AccountDto(
    var id: UUID? = null,
    @NotNull val name: String,
    @NotNull val amount: Double,
    @NotNull val currency: Currency,
    val description: String? = null,
    val addedOn: LocalDate? = null

Next, we add the endpoint in the file

class AccountController @Inject constructor(private val accountService: AccountService) {
    fun getAccounts(@PathVariable(ACCOUNT_ID) accountId: UUID) : ResponseEntity<AccountDto> {
        return ResponseEntity.ok(accountService.getAccount(accountId).toDto())

There is already a lot happening here. The constants for the Path variable and URL definitions are in a file for constants

object URL {
    object AccountUrl {
        const val ACCOUNTS = "/accounts"
        const val GET_ACCOUNT = "$ACCOUNTS/$ACCOUNT_ID"    
    object PathVariable {
        const val ACCOUNT_ID = "accountId"

The more interesting path though is that toDto() bit. This was not defined in our Account class, was it?

Indeed, it was not. Nor should it be - the domain is not allowed any knowledge about the APIs. So where is this coming from?

Well, I did not want to use a Mapper for the conversion to, and from, DTO. It would simply be another bean to inject in places, and, depending on the naming, this can become quite confusing. Therefore, I have used an extension function, a pretty sweet feature that Kotlin offers. In essence, these are methods that are defined elsewhere, but during compilation, are added to the initial class. This is an awesome way to extend classes that you technically do not have any access to. Here is an example:

fun String.iAmAHappyExtension(): String {
    return "I am a happy extension, and I want to say hello!!"

As you can see in the Test result below, you have now successfully extended, or at least added functionality, to the String class.

Extension function test
Now, who's to say String is not extendable?

Obviously, there are better usecases than what you can see above, but the idea is always the same.

So let’s add an extension for our Account class in (Kotlin file, there will be several methods in here):

fun Account.toDto() = AccountDto(
    id =,
    name =,
    amount = this.amount,
    currency = this.currency,
    description = this.description,
    addedOn = this.addedOn

You can notice how the return type is not actually defined for this function. This is due to Kotlin’s type inference, and it’s quite handy, as you do not need to define the return type for functions (and also in declarations) when the variable can only ever be of one class.

Now, you may argue that having a Mapper is actually cleaner, but I really like the conciseness of this pattern.

With all this set up, I’m sure you’d like to test this already. The best way would be to spin up the database, insert an account manually, and then call the endpoint. Another option would be to run the application, and insert an account during startup, and call the endpoint. Since we’re going to add an endpoint to create new accounts anyway though, I will simply do this and then test both endpoints together.

In starting with the opposite extension method:

fun AccountDto.toDomain() = Account(
    name =,
    amount = this.amount,
    currency = this.currency,
    description = this.description

You may notice there are less mappings. Well, for starters, we do not receive the id from the dto, so we can already discared it. It will simply be set with a random value during instantiation. The same logic applies to the addedOn field. Even if we add updates at some point, the addedOn value should not be changed then, either, so this looks great to start.

Now, something pretty cool happened during this development. The compiler complained to me with the description field, that the types did not match. Indeed, the description should be a nullable field in the DTO, but also in the domain object. I had however marked it as String, which cannot be null in Kotlin. This is the feature of Kotlin that will protect you from NPEs, and it’s extremely useful!

A quick fix to that field in the domain object is obvious, it should be changed to @Column(name = "description") val description: String? = null,.

With that out of the way, we add the next endpoint to the controller:

    fun createAccount(accountDto: AccountDto): ResponseEntity<AccountDto> {
        val account = accountService.createAccount(accountDto.toDomain());
        return ResponseEntity.status(HttpStatus.CREATED).body(account.toDto());

There is not much complicated going on here. I know that some people feel like you should only return the id of the newly created object, but I don’t really see the advantage on doing this and then requiring a GET to get the created data (e.g. the addOn field). Until the community is clearer about the “best” practice for this case, I’ll use it this way.

That’s it! Let’s try this out then, eh?

Testing (Manual)

Yes, unit tests are very important, and obviously, I will add them soon. However, I realize that this article is already quite long, so I will dedicate a separate article to JUnit and MockK. :)

For now, let’s start Postman and fire some requests at our endpoints!

The body that I will use for the test is here (since you cannot copy from images):

    "name": "MyFirstAccount",
    "amount": 2200,
    "currency": "EUR",
    "description": "I am so proud to have my own money!!"

If we fire this with a POST to http://localhost:8080/accounts, we will of course be greeted with a nice 401 Unauthorized! Since we don’t want to focus on the security just yet, let’s again disable that for the AccountController (by adding "/accounts", "/accounts/*" in the antmatching).

Let’s try again.

Create account Postman request
Creating an account

Now, copy the ID that you get from the response, and fire a GET to http://localhost:8080/accounts/{yourUUID}

Retrieve account Postman request
Retrieving that very same account works now.

There you have it! You have built two endpoints with which you can create and query accounts! Pretty neat, isn’t it? IF you feel up to it, you can now try connecting your frontend application to the backend, to make sure that the APIs match, for instance. :)

Happy coding!