-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* WIP * WIP * WIP * Images * Editing
- Loading branch information
Showing
8 changed files
with
297 additions
and
3 deletions.
There are no files selected for viewing
282 changes: 282 additions & 0 deletions
282
articles/building-apps/application-layer/application-services.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
--- | ||
title: Application Services | ||
description: How to design application services. | ||
order: 10 | ||
--- | ||
|
||
= Application Services | ||
|
||
The application layer API consists of _application services_. In a simple Vaadin application, the application services reside inside a single <<{articles}/building-apps/architecture/components#,system component>>, as illustrated on the following diagram: | ||
|
||
image::images/application-services.png[The presentation layer calls three application services] | ||
|
||
In code, the services are Spring Beans, and the components Java packages. | ||
|
||
Services should have _high cohesion_. This means that all the methods in your service should relate to the same thing. For example, all methods in this `PaymentService` are directly related to the business activity of handling payments: | ||
|
||
[source,java] | ||
---- | ||
public interface PaymentService { | ||
PaymentId processPayment(Order order); | ||
PaymentStatus checkPaymentStatus(PaymentId paymentId); | ||
void refundPayment(PaymentId paymentId); | ||
} | ||
---- | ||
|
||
Application services need not be related to business activities to be highly cohesive. They could also be related to a specific view in the user interface, or even a specific user interface component. | ||
|
||
For example, if you are using <<{articles}/components/charts#,Vaadin Charts>>, it is easier if the data returned by your service can be directly converted into <<{articles}/components/charts/data#,chart data>>. If you are building a payment summary view, you might be tempted to add such a method to the `PaymentService`. | ||
|
||
However, adding this method to the `PaymentService` would actually lower the cohesion of the service. Although it retrieves information about payments, its primary driver is the needs of the summary view, not the business activity of handling payments. | ||
|
||
Another way of reasoning about the cohesion of services is that any single service should only have a single reason to change. With the payment service containing methods both for handling payments, and for showing the summary, it now has two reasons to change. | ||
|
||
In a situation like this, you should create a new `PaymentSummaryService`, and implement the method there. You now have two services with only a single reason to change. In other words, they are highly cohesive. | ||
|
||
// TODO I want to write something about service methods being atomic and stand-alone, and that a service method should never call another, but I don't know how to formulate it. Maybe under its own section. | ||
|
||
== Interfaces or Classes? | ||
|
||
In the early days of Spring, services consisted of both an interface, and an implementation class. The reason for this is that Spring uses proxies to handle various cross-cutting concerns. At that time, you could only create proxies of Java interfaces, not classes. Furthermore, when writing tests, you could only mock interfaces, not classes. | ||
|
||
Nowadays, this limitation is gone. As long as your service classes are not `final`, and do not contain any `final` methods, Spring can make proxies of them. You can also mock them during tests. | ||
|
||
Because of this, creating interfaces for application services is a matter of personal taste. If you like to create interfaces, you can continue to do so. In that case, you should make the implementation class package protected, like this: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
class PaymentServiceImpl implements PaymentService { | ||
... | ||
} | ||
---- | ||
|
||
However, you can also write the application service without an interface, like this: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
public class PaymentService { | ||
... | ||
} | ||
---- | ||
|
||
The advantage with this approach is that you have one Java file less to maintain. Both approaches are suitable for Vaadin. | ||
|
||
== Input and Output | ||
|
||
Application services often need to communicate with <<{articles}/building-apps/application-layer/persistence/repositories#,repositories>> to fetch and store data. They also need to pass this data to the presentation layer. For this, there are two options: pass the entities directly, or pass Data Transfer Objects (DTO:s). Both have their own pros and cons. | ||
|
||
=== Entities | ||
|
||
When the application service passes the entities directly to the presentation layer, the entities become a part of the application layer API. Many service methods delegate to the corresponding repository methods, for example like this: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
public class CustomerCrudService { | ||
private final CustomerRepository repository; | ||
CustomerCrudService(CustomerRepository repository) { | ||
this.repository = repository; | ||
} | ||
public Page<Customer> findAll(Specification<Customer> specification, Pageable pageable) { | ||
return repository.findAll(specification, pageable); | ||
} | ||
public Customer save(Customer customer) { | ||
return repository.saveAndFlush(customer); | ||
} | ||
} | ||
---- | ||
|
||
[CAUTION] | ||
When most of your service methods delegate to a repository, it may feel tempting to skip the service and have the presentation layer communicate with the repository directly. However, this is not a good idea because of the cross-cutting concerns that the application service should handle. This is explained later on this page. | ||
|
||
Using entities in your application service is a good idea when your user interface and entities match each other closely. For example, you could have a form whose fields, or a grid whose columns, match the fields of the entity. | ||
|
||
It is also a good idea when your entities are _anemic_, which means that they only contain data and little to no business logic. | ||
|
||
In both of these cases, the user interface and the entities are likely to change at the same time, for the same reason. For example, if you need to add a field, you'll add it both to the user interface, and the entity. | ||
|
||
=== Data Transfer Objects | ||
|
||
Sometimes, it is not a good idea for the application services to return the entities themselves. | ||
|
||
For instance, the domain model may contain business logic that must be called within some context that is not available in the presentation layer. It might require access to other services, or run inside a transaction. | ||
|
||
In other cases, the user interface may need only a subset of the data stored inside a single entity, or a combination of data from multiple entities. Fetching and returning the full entities would be a waste of resources. | ||
|
||
You may also have a situation where the domain model and user interface are changing independently of each other. For example, the domain model may have to be adjusted every year due to government regulations while the user interface remains more or less the same. | ||
|
||
In this case, the application services should accept DTO:s as input, and return DTO:s as output. The entities should no longer be part of the application layer API. | ||
|
||
This adds another responsibility to the application service: mapping between entities and DTO:s. | ||
|
||
If you are using <<{articles}/building-apps/application-layer/persistence/repositories#query-objects,query objects>>, you can do the mapping in them by returning their DTO:s directly. In this case, the query object DTO:s become part of the application layer API. | ||
|
||
For storing data, your services typically have to copy data from the DTO to the entity. For example, like this: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
public class CustomerCrudService { | ||
private final CustomerRepository repository; | ||
CustomerCrudService(CustomerRepository repository) { | ||
this.repository = repository; | ||
} | ||
// In this example, CustomerForm is a Java record. | ||
public CustomerForm save(CustomerForm customerForm) { | ||
var entity = Optional.ofNullable(customerForm.getId()) | ||
.flatMap(repository::findById) | ||
.orElseGet(Customer::new); | ||
entity.setName(customerForm.name()); | ||
entity.setEmail(customerForm.email()); | ||
... | ||
return toCustomerForm(repository.saveAndFlush(entity)); | ||
} | ||
private CustomerForm toCustomerForm(Customer entity) { | ||
return new CustomerForm(entity.getId(), entity.getName(), entity.getEmail(), ...); | ||
} | ||
} | ||
---- | ||
|
||
When using DTO:s, you have more code to maintain. Also, some changes, like adding a new field to the application, requires more work. However, your user interface and domain model are isolated from each other, and can evolve independently. | ||
|
||
=== Domain Payload Objects | ||
|
||
If you are using <<{articles}/building-apps/application-layer/domain-primitives#,domain primitives>>, you can, and should, use them in your DTO:s as well. In this case, the DTO:s are called _Domain Payload Objects_ (DPO). They are used in the exact same way as DTO:s. | ||
|
||
== Cross-Cutting Concerns | ||
|
||
Application services act as the main entry point into the application from the user interface. Because of this, they have some extra responsibilities in addition to handling the business activities. The most important ones are security, transaction management, and observability. | ||
|
||
image::images/cross-cutting-concerns.png[A call from the presentation layer goes through three boundaries] | ||
|
||
You can implement cross-cutting concerns in two ways. You can use Aspect Oriented Programming (AOP), which is what Spring uses for its cross-cutting concerns. For instance, this is how you would run the `save` method inside a transaction using AOP: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
public class CustomerCrudService { | ||
... | ||
@Transactional | ||
public CustomerForm save(CustomerForm customerForm) { | ||
... | ||
} | ||
} | ||
---- | ||
|
||
During application startup, Spring detects the `@Transactional` annotation and turns the service into a proxy. When a client calls the `save` method, the calls gets routed through a _method interceptor_. The interceptor starts the transaction, calls the actual method, and then commits the transaction when the method returns. | ||
|
||
You can also implement the cross-cutting concerns inside every service method. For instance, this is how you would run the `save` method inside a transaction explicitly: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
public class CustomerCrudService { | ||
private final TransactionTemplate transactionTemplate; | ||
... | ||
public CustomerForm save(CustomerForm customerForm) { | ||
return transactionTemplate.execute(tx -> { | ||
... | ||
}); | ||
} | ||
} | ||
---- | ||
|
||
If you use AOP, you should write integration tests that also test the cross-cutting concerns. If there is a problem with your application context, and your aspect ends up not being applied, you may not notice it until it is too late. | ||
|
||
See the https://docs.spring.io/spring-framework/reference/core/aop.html[Spring Documentation] for more information about AOP. | ||
|
||
[NOTE] | ||
Each cross-cutting concern deserves a documentation page of its own. This section will be updated as new pages are written. | ||
|
||
=== Security | ||
|
||
All application services in your Vaadin application should be protected by Spring Security. You should do this regardless of how your user interface views are protected. Even methods that do not require authentication should be explicitly declared to permit anonymous users. | ||
|
||
To protect application services, you have to enable _method security_. To do that, you need to add the `@EnableMethodSecurity` annotation to your security class. After that, you can use annotations to secure your application services. Spring Boot recommends the use of the `@PreAuthorize` annotation. | ||
|
||
See the https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html[Spring Security Documentation] for more information about method security. | ||
|
||
// TODO Add a link to a separate page about security once written. | ||
|
||
=== Transactions | ||
|
||
All application service methods that touch the database should always run inside their own transactions. You should use the `REQUIRES_NEW` transaction propagation. | ||
|
||
See the https://docs.spring.io/spring-framework/reference/data-access/transaction.html[Spring Documentation] for more information about transaction management. | ||
|
||
// TODO Replace this link with a link to our own documentation page. | ||
|
||
=== Observability | ||
|
||
If you want to observe what your application services are doing from the outside, you can use https://micrometer.io/[Micrometer Observation]. | ||
|
||
See the https://docs.spring.io/spring-boot/reference/actuator/observability.html[Spring Boot Documentation] for more information about observability. | ||
|
||
// TODO Add a link to a separate page about observability | ||
|
||
== Vaadin Integration | ||
|
||
For Flow user interfaces, you inject the application services directly into your views, like this: | ||
|
||
[source,java] | ||
---- | ||
@Route("payment") | ||
public class PaymentView extends VerticalLayout { | ||
private final PaymentService paymentService; | ||
public PaymentView(PaymentService paymentService) { | ||
this.paymentService = paymentService; | ||
... | ||
} | ||
} | ||
---- | ||
|
||
For Hilla, you should make the application services _browser callable_. You do this by adding the `@BrowserCallable` annotation to your service, like this: | ||
|
||
[source,java] | ||
---- | ||
@Service | ||
@BrowserCallable | ||
public class PaymentService { | ||
... | ||
} | ||
---- | ||
|
||
See the <<{articles}/hilla/guides/endpoints#,Hilla endpoints documentation page>> for more information. | ||
|
||
== Scaling | ||
|
||
As the application grows, it makes sense to split the application services component into smaller parts. It is recommended to split the services according to which _bounded context_ they belong to. | ||
|
||
A bounded context is a term from domain-driven design. It is a clear boundary within a system where a specific domain model is defined and consistent. It ensures that within this context, terms and concepts have precise meanings that are not confused or conflicted with other contexts in the system. This separation helps to manage complexity by allowing different parts of the system to evolve independently. | ||
|
||
// TODO Consider adding a separate page about bounded contexts only. | ||
|
||
For example, on this diagram, the presentation layer interacts with three different bounded contexts through their service components: Quotation Management, Order Management, and Customer Relations Management: | ||
|
||
image::images/domain-application-services.png[The presentation calls three different application service components] | ||
|
||
Bounded contexts are often associated with <<{articles}/building-apps/architecture/microservices#,microservices>>. However, you can also use them when you're building <<{articles}/building-apps/architecture/monoliths#,modular monliths>>. This is the recommended approach in Vaadin applications. | ||
|
||
In <<{articles}/building-apps/project-structure/single-module#,single-module projects>>, you should place the bounded contexts into their own packages. The three contexts from the example above would correspond to the following Java packages: | ||
|
||
* `com.example.application.qm.services` (Quotation Management) | ||
* `com.example.application.om.services` (Order Management) | ||
* `com.example.application.crm.services` (Customer Relations Management) | ||
|
||
In <<{articles}/building-apps/project-structure/multi-module#,multi-module projects>>, you should place the bounded contexts into their own Maven modules. | ||
|
||
You may even want to split a single bounded context into multiple Maven modules. For instance, you might want to have the application services and the domain model in two separate modules. |
2 changes: 1 addition & 1 deletion
2
articles/building-apps/application-layer/domain-primitives.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+29.1 KB
articles/building-apps/application-layer/images/application-layer-api.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+35.5 KB
articles/building-apps/application-layer/images/application-services.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+47.4 KB
articles/building-apps/application-layer/images/cross-cutting-concerns.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+56.6 KB
articles/building-apps/application-layer/images/domain-application-services.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
articles/building-apps/application-layer/persistence/index.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters