Three-Layered Architecture (With Example)

Alain Sondrae
10 min readOct 22, 2022

--

The Layered Architecture deals with an application as a monolith, separating it’s logic into different layers. Each of those layers is dependent on the one below it. Despite it being so, the use of Layered Architecture is that each of layers can be changed without having to change the other ones. This especially helps with the Open-Closed principle.

It should not be confused with the Three-Tier or N-Tier Architecture, where each logical component is a different application. Although the idea behind them is similar.

Say you have to build an application for your semester or bachelor project at university, or you have been given the same task at your job, or you just want to work on a project in your spare time. You are not that crazy to start coding straight away, right? That would be too risky and would potentially create design problems so huge you would wish you had taken those several hours to think the thing through.

Example

Let’s take as an example an application where users (potentially a frontend application) make requests to an API to manage their virtual book shelf.

I have come up with a set of simple requirements:

  1. User should be able to create Shelves with names.
  2. User should be able to add books to a shelf.
  3. User should be able to add books, but not to a shelf.
  4. User should be able to see a specific book.
  5. User should be able to get a shelf by it’s id.
  6. User should be able to see al their books.
  7. User should be able to see all their shelves.

Now, lets divide the application into the necessary parts that will deal with the incoming requests, the handling of those requests according to the business needs, and connecting to a database, respectively.

The logic presented above can be coded in any language. For this specific example, I will use Java 11 and Spring Boot.

Going to https://start.spring.io/, it’s relatively easy to create a Spring Boot application, adding the right dependencies.

This is how it looks:

Screenshot from Spring Initializr

After hitting that ‘Generate’ button, save the zip folder where you can easily find it. Then unzip it in the same location.

If you run into problems on opening the project (as I did), try seeing if you have set the path to maven in your settings (File->Setting->Build, Execution, Deployment->Build Tools->Maven->Maven Home Path) for Intellij

This is how your project folder should look now in an IDE. If you start the application straight away, you will most likely see a message that you are missing a Data Source. To fix this, go into main/resources/application.properties and set the data source configuration. For the sake of this example, I will set a PostgreSQL data source.

This is the simple configuration you need to have in the file.

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=YoUR_PasSwOrd

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true

If it still doesn’t work, try adding this:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

Either this, or see the errors you get on start-up. Spring Boot can be tricky sometimes.

Now, you should be able to run the project.

Usually, I prefer to start from the entities that I am going to use in my database. For this, I create a package called ‘model’ (it’s less part of the this topic, rather just a good way of doing things). Here I create my POJO’s (Plain Old Java Objects), with the necessary annotations, one empty constructor and one for my purposes, and getters and setters.

This is how my entities look:

Shelf:

package BookshelfAPI.BookshelfAPI.models;

import javax.persistence.*;
import java.io.Serializable;
import java.util.*;

@Entity
@Table(name = "shelves")

public class Shelf implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int Id;
private String name;
@OneToMany(mappedBy = "shelf", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JsonManagedReference
private List<Book> books;

public Shelf(){}

public Shelf(String name) {
this.name = name;
this.books = new ArrayList<>();
}

public int getId() {
return Id;
}
public void setId(int id) {
Id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Book> getBooks() {
return books;
}
public void setBooks(List<Book> books) {
this.books = books;
}

Book:

package BookshelfAPI.BookshelfAPI.models;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "books")
public class Book implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int Id;
private String title;
private String description;
private int numberOfPages;
private int yearPublished;
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "shelf_id")
@JsonIgnore
private Shelf shelf;

public Book(){}

public Book(String title, String description, int numberOfPages, int yearPublished, Shelf shelf) {
this.title = title;
this.description = description;
this.numberOfPages = numberOfPages;
this.yearPublished = yearPublished;
this.shelf = shelf;
}

public int getId() {
return Id;
}
public void setId(int id) {
Id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getNumberOfPages() {
return numberOfPages;
}
public void setNumberOfPages(int numberOfPages) {
this.numberOfPages = numberOfPages;
}
public int getYearPublished() {
return yearPublished;
}
public void setYearPublished(int yearPublished) {
this.yearPublished = yearPublished;
}
public Shelf getShelf() {
return shelf;
}
public void setShelf(Shelf shelf) {
this.shelf = shelf;
}
}

Notice that Shelf and Book have a One-to-Many relationship (one shelf per many books). This is easily done using the annotations.

After creating the entities, I run the application again to see how Hibernate creates the tables in the database.

Now, having the previously stated requirements, I add the three layers that we have talked about before, namely: controllers (presentation), service(business logic), and repositories(data).

This is how the packaging looks now:

App packaging

As you can see, I have also added a ‘dtos’ package, for using DTOs (Data Transfer Objects) for getting data through controllers.

I haven’t seen any specific guideline for where one should start with writing code. For some it makes sense to start with controllers, where the data comes through, and then creating other classes going further with their logic. For others it is logical to write the interfaces for the repositories, that basically provide the CRUD operations on the database. It also makes sense to start with the services, as these classes basically deal with the business logic of the application.

One thing should be clear, every layer depends on the one below it. Because of this, it makes sense to me to start from creating the repositories.

ShelfRepository:

package BookshelfAPI.BookshelfAPI.repositories;

import BookshelfAPI.BookshelfAPI.models.Shelf;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ShelfRepository extends JpaRepository<Shelf, Integer> {
}

Basically ,the repositories are simple interfaces, that either extend the JpaRepository<T, Id> or the CrudRepository<T, Id>. ‘T’ is the entity you wish to deal with by using this interface, and ‘Id’ is the data type you have defined as the entity’s Id. Moreover, it is possible to define extra methods for this interface, besides the ones that Spring provides. They won’t be needed here, however.

The BookRepository is coded the same way as the other one.

Now, to the business logic of handling the shelves.

The shelves should be created; a user can see a shelf by it’s id and then they should be able to see al their shelves.

As for books, it is possible to add books (either to a shelf or not), see all the books and see a specific book.

This is how the ‘service’ package looks like:

service package

It is considered good practice by many programmers to use the SOLID principles when coding. Because of this, there are interfaces that define the methods that other classes can use, and implementations to these interfaces. This way, the implementation can always be changed easily (for example one wants to use something else than the repositories).

This are the ShelfService and the ShelfServiceImpl:

package BookshelfAPI.BookshelfAPI.service;
import BookshelfAPI.BookshelfAPI.models.Shelf;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public interface ShelfService {
void createShelf(Shelf shelf);
Shelf getShelfById(int shelfId);
List<Shelf> getAllShelves();
}
package BookshelfAPI.BookshelfAPI.service;
import BookshelfAPI.BookshelfAPI.models.Shelf;
import BookshelfAPI.BookshelfAPI.repositories.ShelfRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
public class ShelfServiceImpl implements ShelfService{

@Autowired
ShelfRepository shelfRepository;

@Override
public void createShelf(Shelf shelf) {
shelfRepository.save(shelf);
}

@Override
public Shelf getShelfById(int shelfId) {
if(shelfRepository.findById(shelfId).isPresent()){
return shelfRepository.findById(shelfId).get();
}
return null;
}

@Override
public List<Shelf> getAllShelves() {
return shelfRepository.findAll();
}
}

The business logic to the shelf service is quite simple. It merely uses the methods provided by the repositories.

And here is the BookService:

package BookshelfAPI.BookshelfAPI.service;
import BookshelfAPI.BookshelfAPI.dtos.BookDto;
import BookshelfAPI.BookshelfAPI.models.Book;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
public interface BookService {
void addBook(BookDto bookDto);
List<Book> getAllBooks();
Book getBookById(int bookId);
}
package BookshelfAPI.BookshelfAPI.service;
import BookshelfAPI.BookshelfAPI.dtos.BookDto;
import BookshelfAPI.BookshelfAPI.models.Book;
import BookshelfAPI.BookshelfAPI.models.Shelf;
import BookshelfAPI.BookshelfAPI.repositories.BookRepository;
import BookshelfAPI.BookshelfAPI.repositories.ShelfRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class BookServiceImpl implements BookService{

@Autowired
private BookRepository bookRepository;
@Autowired
private ShelfRepository shelfRepository;

@Override
public void addBook(BookDto bookDto) {
Book book = null;
if(bookDto.getShelfId()!=0){
Shelf shelfForBook = null;
boolean shelfExists = shelfRepository.findById(bookDto.getShelfId()).isPresent();
if(shelfExists){
shelfForBook = shelfRepository.findById(bookDto.getShelfId()).get();
book = new Book(bookDto.getTitle(), bookDto.getDescription(), bookDto.getNumberOfPages(), bookDto.getYearPublished(), shelfForBook);
}
}else{
book = new Book(bookDto.getTitle(), bookDto.getDescription(), bookDto.getNumberOfPages(), bookDto.getYearPublished());
}
bookRepository.save(book);
}

@Override
public List<Book> getAllBooks() {
return bookRepository.findAll();
}

@Override
public Book getBookById(int bookId) {
if(bookRepository.existsById(bookId)){
return bookRepository.findById(bookId).get();
}
return null;
}
}

The BookSericeImpl makes use of both the BookRepository and the ShelfRepository, as it needs to check if a shelf exists first before adding the book it.

Moreover, Spring allows to inject dependencies by using the @Autowired annotation.

Now, these services can be used by the controllers, with which other applications can interact.

Before that, here are the dtos that are used by the controllers.

ShelfDto:

package BookshelfAPI.BookshelfAPI.dtos;
import BookshelfAPI.BookshelfAPI.models.Shelf;
import java.io.Serializable;

public class ShelfDto implements Serializable {
private String name;

public ShelfDto(){}
public ShelfDto(String name) {
this.name = name;
}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

BookDto:

package BookshelfAPI.BookshelfAPI.dtos;
import org.springframework.lang.Nullable;
import java.io.Serializable;

public class BookDto implements Serializable {
private String title;
private String description;
private int numberOfPages;
private int yearPublished;
@Nullable
private int shelfId;

public BookDto (){}
public BookDto(String title, String description, int numberOfPages, int yearPublished) {
this.title = title;
this.description = description;
this.numberOfPages = numberOfPages;
this.yearPublished = yearPublished;
}

public BookDto(String title, String description, int numberOfPages, int yearPublished, int shelfId) {
this.title = title;
this.description = description;
this.numberOfPages = numberOfPages;
this.yearPublished = yearPublished;
this.shelfId = shelfId;
}

public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getNumberOfPages() {
return numberOfPages;
}
public void setNumberOfPages(int numberOfPages) {
this.numberOfPages = numberOfPages;
}
public int getYearPublished() {
return yearPublished;
}
public void setYearPublished(int yearPublished) {
this.yearPublished = yearPublished;
}
public int getShelfId() {
return shelfId;
}
public void setShelfId(int shelfId) {
this.shelfId = shelfId;
}
}

The BookDto has two constructors with different parameters, because books can be created with or without a shelf Id.

Here are the controllers:

ShelfController:

package BookshelfAPI.BookshelfAPI.controllers;
import BookshelfAPI.BookshelfAPI.dtos.ShelfDto;
import BookshelfAPI.BookshelfAPI.models.Shelf;
import BookshelfAPI.BookshelfAPI.service.ShelfService;
import BookshelfAPI.BookshelfAPI.service.ShelfServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;


@RestController
public class ShelfController {

@Autowired
private ShelfService shelfService;

public ShelfController() {
this.shelfService = new ShelfServiceImpl();
}

@PostMapping("/shelf")
public ResponseEntity createBookShelf(@RequestBody ShelfDto shelfDto){

Shelf newShelf = new Shelf(shelfDto.getName());

shelfService.createShelf(newShelf);

return new ResponseEntity(HttpStatus.OK);
}

@GetMapping("shelves")
public ResponseEntity<List<Shelf>> getAllShelves(){

List<Shelf> shelves = shelfService.getAllShelves();

return ResponseEntity.status(HttpStatus.OK).body(shelves);
}

@GetMapping("shelf/{id}")
public ResponseEntity<Shelf> getShelfById(@PathVariable("id") int id){
Shelf shelf = shelfService.getShelfById(id);

return ResponseEntity.status(HttpStatus.OK).body(shelf);
}
}

BookController:

package BookshelfAPI.BookshelfAPI.controllers;
import BookshelfAPI.BookshelfAPI.dtos.BookDto;
import BookshelfAPI.BookshelfAPI.models.Book;
import BookshelfAPI.BookshelfAPI.service.BookService;
import BookshelfAPI.BookshelfAPI.service.ShelfService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.*;
@RestController
public class BooksController {

@Autowired
private BookService bookService;
@Autowired
private ShelfService shelfService;

@PostMapping("book")
public ResponseEntity addBook(@RequestBody BookDto bookDto){

bookService.addBook(bookDto);

return new ResponseEntity(HttpStatus.OK);
}

@GetMapping("books")
public ResponseEntity<List<Book>> getAllBooks(){

List<Book> allBooks = bookService.getAllBooks();

return ResponseEntity.status(HttpStatus.OK).body(allBooks);
}

@GetMapping("books/{id}")
public ResponseEntity<Book> getBookById(@PathVariable int id){

Book bookToReturn = bookService.getBookById(id);

if (bookToReturn==null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}

return ResponseEntity.status(HttpStatus.OK).body(bookToReturn);
}
}

The controllers simply make use of the methods that were previously defined in the services, and return them to the user.

To test the API, I use Postman.

In the following steps I will test all of the requirements that have been stated before:

  1. Create shelves:
Postman: create a shelf

A shelf has a name. I give this in the body of the post request that I am sending. The answer is 200, which means it worked.

2. Adding books to a shelf

Postman: Adding book to a shelf (with shelf Id)

3. Adding a book

Postman: Adding a book (without shelf id)

4. See specific book

Postman: See a specific book by it’s id

5. Get a shelf by it’s id

Postman: get a shelf by id

6. See all books

Postman: All books

7. See all shelves

Postman: All shelves

Conclusion

In this (maybe not that short) article the three-layer architecture was explained. It is similar in design to MVVM, thus it’s widely known. Moreover, the example above shows how it is used, and how each logical part of the application is separated into it’s own module. I hope you have enjoyed this example and found it useful.

To view the whole application’s code and to clone it, follow this GitHub link: https://github.com/asold2/BookshelfAPI

--

--