← Back to all Posts

How to use a layered architecture

Lets assume the following layered architecture of our application.

Example of a layered architecture

Our Application has two entry points, the command line and the http interface (either a request for a html document or for json data). Furthermore they are both using the same business logic. The Business Logic has access to the Database and the Filesystem to store and retrieve data or files.

The Physical Infrastructure is an representation of the database and the filesystem. It is not part of the application, but it is needed to run the application. Depending on the environment the application is running in, the physical infrastructure can be different. For example in a test environment the database can be an in memory database, while in production it is a real database.

We assume that we are using MySQL and an S3-compatible Filesystem in the Production environment and during Development we are using sqlite and the local filesystem.

Contracts between the Layers

The layers are communicating with each other via contracts. A contract is an interface, which defines the methods that can be called. The contract is implemented by the layer that is using it. The layer that is using the contract is called the consumer and the layer that is implementing the contract is called the provider.

Lets assume we are developing a simple blog where you can create, edit and delete posts and also manage the users of the blog.

The Database Component

For that we need functionality the ability to list, retrieve, store, update and delete posts and users to/from the database. This could be a simple contract for the Database Componenent:

Database/UserRepository
    - all(): User[]
    - find(id: int): User
    - store(user: User): void
    - update(user: User): void
    - delete(user: User): void

Database/User
    - id: int
    - name: string
    - email: string
    - password: string (hashed)
    - createdAt: DateTime
    - updatedAt: DateTime
Database/PostRepository
    - all(): Post[]
    - find(id: int): Post
    - store(post: Post): void
    - update(post: Post): void
    - delete(post: Post): void

Database/Post
    - id: int
    - title: string
    - content: string
    - files: File[]
    - createdAt: DateTime
    - updatedAt: DateTime

The Database Component is the provider of the contract and the Business Logic Component is the consumer of the contract.

The Filesystem Component

For the filesystem we want the ability to retrieve, store and delete files, which can be used in blog posts (for example images or pdf files for download). This could be a simple contract for the Filesystem Component:

Filesystem/FileRepository
    - get(path: string): File
    - store(path: string, file: File): void
    - delete(path: string): void

Filesystem/File
    - path: string
    - content: string

The Filesystem Component is the provider of the contract and the Business Logic Component is the consumer of the contract.

The Business Logic Component

The Business Logic Component is the consumer of the Database and Filesystem Component. It is also the provider of the contract for the Command Line and Http Interface.

We assume that our Business Logic has single responsibility classes for each use case. For example a class for creating a post, a class for editing a post and a class for deleting a post. The same applies for the users.

BusinessLogic/Users/ListUsers
    - canBeExecutedBy(user: User): bool
    - execute(): User[]

BusinessLogic/Users/ShowUser
    - canBeExecutedBy(user: User): bool
    - execute(id: int): User

BusinessLogic/Users/CreateUser
    - canBeExecutedBy(user: User): bool
    - validate(name: string, email: string, password: string): bool
    - execute(name: string, email: string, password: string): User

BusinessLogic/Users/EditUser
    - canBeExecutedBy(user: User): bool
    - validate(user: User, name: string, email: string, password: string): bool
    - execute(user: User, name: string, email: string, password: string): User

BusinessLogic/Users/DeleteUser
    - canBeExecutedBy(user: User): bool
    - validate(user: User): bool
    - execute(user: User): void
BusinessLogic/Posts/ListPosts
    - canBeExecutedBy(user: User): bool
    - execute(): Post[]

BusinessLogic/Posts/ShowPost
    - canBeExecutedBy(user: User): bool
    - execute(id: int): Post

BusinessLogic/Posts/CreatePost
    - canBeExecutedBy(user: User): bool
    - validate(title: string, content: string, files: File[]): bool
    - execute(title: string, content: string, files: File[]): Post

BusinessLogic/Posts/EditPost
    - canBeExecutedBy(user: User): bool
    - validate(post: Post, title: string, content: string, files: File[]): bool
    - execute(post: Post, title: string, content: string, files: File[]): Post

BusinessLogic/Posts/DeletePost
    - canBeExecutedBy(user: User): bool
    - validate(post: Post): bool
    - execute(post: Post): void

The Console Component

The Console Component is a consumer of the Business Logic Component.

Here you can only create, update or delete users, but not posts. This could be a simple contract for the Console Component:

Console/Users/CreateUserCommand
    - execute(args: string[]): void

Console/Users/EditUserCommand
    - execute(args: string[]): void

Console/Users/DeleteUserCommand
    - execute(args: string[]): void

We assume these commands can be called as follows:

app/console user:create <name> <email> <password>
app/console user:edit <id> <name> <email> <password>
app/console user:delete <id>

The Http Component

The Http Component is a consumer of the Business Logic Component.

We have two interfaces available where requests can be coming to. One is for the browser, where we want to return a html document and the other one is for a mobile app, where we want to return json data.

Depending on where the request is coming from we have different authentication methods. For example if the request is coming from the browser, we can use a session cookie to authenticate the user. If the request is coming from a mobile app, we can use a token to authenticate the user.

This could be a simple contract for the Http Component:

Http/Users/ListUsersController
    - viewList(request: WebRequest): HtmlResponse
    - listApi(request: ApiRequest): JsonResponse

Http/Users/ShowUserController
    - viewShow(request: WebRequest): HtmlResponse
    - showApi(request: ApiRequest): JsonResponse

Http/Users/CreateUserController
    - viewForm(request: WebRequest): HtmlResponse
    - storeWeb(request: WebRequest): HtmlResponse
    - storeApi(request: ApiRequest): JsonResponse

Http/Users/EditUserController
    - viewForm(request: WebRequest): HtmlResponse
    - updateWeb(request: WebRequest): HtmlResponse
    - updateApi(request: ApiRequest): JsonResponse

Http/Users/DeleteUserController
    - viewForm(request: WebRequest): HtmlResponse
    - deleteWeb(request: WebRequest): HtmlResponse
    - deleteApi(request: ApiRequest): JsonResponse
Http/Posts/ListPostsController
    - viewList(request: WebRequest): HtmlResponse
    - listApi(request: ApiRequest): JsonResponse

Http/Posts/ShowPostController
    - viewShow(request: WebRequest): HtmlResponse
    - showApi(request: ApiRequest): JsonResponse

Http/Posts/CreatePostController
    - viewForm(request: WebRequest): HtmlResponse
    - storeWeb(request: WebRequest): HtmlResponse
    - storeApi(request: ApiRequest): JsonResponse

Http/Posts/EditPostController
    - viewForm(request: WebRequest): HtmlResponse
    - updateWeb(request: WebRequest): HtmlResponse
    - updateApi(request: ApiRequest): JsonResponse

Http/Posts/DeletePostController
    - viewForm(request: WebRequest): HtmlResponse
    - deleteWeb(request: WebRequest): HtmlResponse
    - deleteApi(request: JsonRequest): JsonResponse

Now we only need to map the request to the right controller and the right method. For example:

// Web Request
| Method | Path             | Controller::Method                        |
| ------ | ---------------- | ----------------------------------------- |
| GET    | /users           | Http/Users/ListUsersController::viewList  |
| GET    | /users/create    | Http/Users/CreateUserController::viewForm |
| GET    | /users/{id}      | Http/Users/ShowUserController::viewShow   |
| GET    | /users/{id}/edit | Http/Users/EditUserController::viewForm   |
| POST   | /users           | Http/Users/CreateUserController::storeWeb |
| PUT    | /users/{id}      | Http/Users/EditUserController::updateWeb  |
| DELETE | /users/{id}      | Http/Users/EditUserController::updateWeb  |

// API Request
| Method | Path             | Controller::Method                        |
| ------ | ---------------- | ----------------------------------------- |
| GET    | /api/users       | Http/Users/ListUsersController::listApi   |
| GET    | /api/users/{id}  | Http/Users/ShowUserController::showApi    |
| POST   | /api/users       | Http/Users/CreateUserController::storeApi |
| PUT    | /api/users/{id}  | Http/Users/EditUserController::updateApi  |
| DELETE | /api/users/{id}  | Http/Users/EditUserController::updateApi  |
// Web Request
| Method | Path             | Controller::Method                        |
| ------ | ---------------- | ----------------------------------------- |
| GET    | /posts           | Http/Posts/ListPostsController::viewList  |
| GET    | /posts/create    | Http/Posts/CreatePostController::viewForm |
| GET    | /posts/{id}      | Http/Posts/ShowPostController::viewShow   |
| GET    | /posts/{id}/edit | Http/Posts/EditPostController::viewForm   |
| POST   | /posts           | Http/Posts/CreatePostController::storeWeb |
| PUT    | /posts/{id}      | Http/Posts/EditPostController::updateWeb  |
| DELETE | /posts/{id}      | Http/Posts/EditPostController::updateWeb  |

// API Request
| Method | Path             | Controller::Method                        |
| ------ | ---------------- | ----------------------------------------- |
| GET    | /api/posts       | Http/Posts/ListPostsController::listApi   |
| GET    | /api/posts/{id}  | Http/Posts/ShowPostController::showApi    |
| POST   | /api/posts       | Http/Posts/CreatePostController::storeApi |
| PUT    | /api/posts/{id}  | Http/Posts/EditPostController::updateApi  |
| DELETE | /api/posts/{id}  | Http/Posts/EditPostController::updateApi  |

Implementing the contracts

Now you can start implementing the contracts according to the definition.

How could for example the implementation of the Http/Users/ListUsersController look like?

Http/Users/ListUsersController
use BusinessLogic/Users/ListUsers as ListUsers

viewList(request: WebRequest): HtmlResponse
{
    // returns the user from the session cookie
    user = request->getUser()

    if user is null
        then return HtmlResponse(
            status: 401,
            content: "Unauthenticated"
        )

    if not ListUsers::canBeExecutedBy(user)
        then return HtmlResponse(
            status: 403,
            content: "Forbidden"
        )

    users = ListUsers::execute()

    return HtmlResponse(
        status: 200,
        content: render("users/list", users: users)
    )
}
Http/Users/ListUsersController
use BusinessLogic/Users/ListUsers as ListUsers

listApi(request: ApiRequest): JsonResponse
{
    // returns the user from the Bearer token
    user = request->getUser()

    if user is null
        then return JsonResponse(
            status: 401, 
            content: "Unauthenticated"
        )

    if not ListUsers::canBeExecutedBy(user)
        then return JsonResponse(
            status: 403,
            content: "Forbidden"
        )

    users = ListUsers::execute()

    return JsonResponse(
        status: 200,
        data: users
    )
}

How could the implementation of the Http/Users/CreateUserController look like?

Http/Users/CreateUserController
use BusinessLogic/Users/CreateUser as CreateUser

viewForm(request: WebRequest): HtmlResponse
{
    // returns the user from the session cookie
    user = request->getUser()

    if user is null
        then return HtmlResponse(
            status: 401,
            content: "Unauthenticated"
        )

    if not CreateUser::canBeExecutedBy(user)
        then return HtmlResponse(
            status: 403,
            content: "Forbidden"
        )

    return HtmlResponse(
        status: 200,
        content: render("users/create")
    )
}
Http/Users/CreateUserController
use BusinessLogic/Users/CreateUser as CreateUser

storeWeb(request: WebRequest): HtmlResponse
{
    // returns the user from the session cookie
    user = request->getUser()

    if user is null
        then return HtmlResponse(
            status: 401,
            content: "Unauthenticated"
        )

    if not CreateUser::canBeExecutedBy(user)
        then return HtmlResponse(
            status: 403,
            content: "Forbidden"
        )

    name = request->get("name")
    email = request->get("email")
    password = request->get("password")

    if not CreateUser::validate(name, email, password)
        then return HtmlResponse(
            status: 400,
            content: "Bad Request"
        )

    user = CreateUser::execute(name, email, password)

    return HtmlResponse(
        status: 302,
        content: redirect("users/{user.id}")
    )
}
Http/Users/CreateUserController
use BusinessLogic/Users/CreateUser as CreateUser

storeApi(request: ApiRequest): JsonResponse
{
    // returns the user from the Bearer token
    user = request->getUser()

    if user is null
        then return JsonResponse(
            status: 401,
            content: "Unauthenticated"
        )

    if not CreateUser::canBeExecutedBy(user)
        then return JsonResponse(
            status: 403,
            content: "Forbidden"
        )

    name = request->get("name")
    email = request->get("email")
    password = request->get("password")

    if not CreateUser::validate(name, email, password)
        then return JsonResponse(
            status: 400,
            content: "Bad Request"
        )

    user = CreateUser::execute(name, email, password)

    return JsonResponse(
        status: 201,
        data: user
    )
}

Conclusion

In our example you always have the same steps you follow when implementing a new feature:

  1. Define the contract
  2. Implement the contract
  3. Map the request to the right controller and method

This makes it easy to add new features to the application and also to find the right place to implement the feature.

In our Http Component all the controllers follow the same pattern:

  1. Get the request user (if needed: web from session cookie, api from bearer token)
  2. Check if the user is authenticated (if needed)
  3. Check if the user is authorized to execute the action (if needed)
  4. Validate the request data (if needed)
  5. Execute the action
  6. Return the response (either a html document or json data)

This makes it easy to understand how the Http Component works and also makes it easy to add new controllers.

This can be applied to other applications, were you have different entry points into the system. For example a command line interface and a http interface. Or a command line interface and a message queue. Or a http interface and a message queue. Or a command line interface, a http interface and a message queue.

My Homepage ·Contact · Privacy Policy
© 2025 Julius Kiekbusch