Domain Services

Introduction

Domain services are (usually) singleton stateless services that provide act upon entities or view models.

Domain services fall into two main categories: those that part of the Causeway metamodel, and those that are not.

  • For the former, their actions (which follow the same conventions as other domain objects) will typically be rendered either in the menu; or they might be surfaced through the REST API. However, a service can only declare actions; it cannot have properties nor can it have collections.

  • For the latter, these are often repositories or factories. Evans' Domain Driven Design, draws a clear distinction between a factory (that creates object) and a repository (that is used to find existing objects). For example, for the Customer entity there may be a CustomerRepository and an CustomerFactory or an OrderFactory; .

On the other hand, from an end-users' perspective the act of finding an existing object vs creating a new one are quite closely related. For this reason, in Apache Causeway it’s therefore quite common to have a single menu domain service that delegates programmatically to other factory or repository services.

Sometimes though a domain service might be holders of business logic that for whatever reason you want to keep outside an entity; perhaps it easier to test that way. An example might be InvoiceCalculationServices.

The behaviour of these services is rendered in various ways, though the most obvious is as the menu actions on the top-level menu bars in the Web UI (Wicket viewer)'s UI.

Domain services can also be used for a number of other purposes:

  • to provide additional non-UI functionality; an example being to perform an address geocoding lookup against the google-maps API, or to perform some calculation, or attach a barcode, send an email etc

  • to act as a subscribers to the event bus, potentially influencing events fired by some other module (a key technique for decoupling large applications)

    This is discussed in more detail below, in the section on events.

  • to implement an SPI of the Apache Causeway framework, most notably cross-cutting concerns such as security, command profiling, auditing and publishing.

Domain objects of any type (entities, other services, view models, mixins) can also delegate to domain services; domain services are automatically injected into every other domain object. This injection of domain services into entities is significant: it allows business logic to be implemented in the domain entities, rather than have it "leach away" into supporting service layers. Said another way: it is the means by which Apache Causeway helps you avoid the anaemic domain model anti-pattern.

Domain services are instantiated once and once only by the framework, and are used to centralize any domain logic that does not logically belong in a domain entity or value.

@DomainService vs @Service/@Repository

Declaring a Domain Service

Domain services that are visible in the UI or REST API are annotated with @DomainService(), while services that are programmatic in nature should be simply annotated using Spring’s @Component or one of its specializations, eg @Service or @Repository.

Once declared, domain services can be injected into other domain objects (including entities and view models) using @javax.inject.Inject. There’s more on this topic below.

Nature of Service

Apache Causeway uses Spring Boot to instantiate and manage the dependency injection of domain services. Accordingly, all domain services are annotated or meta-annotated using Spring’s @Component annotation.

For domain services to be visible in the Apache Causeway UI, they must be annotated with @DomainService. Its #nature() attribute is either:

It’s also possible to define a "programmatic" domain service, meaning one that is instantiated and injected by Spring Boot, but is not visible in the UI or REST API. Such programmatic services are usually annotated with Spring’s @Service annotation or @Repository.

Framework-defined services

As well as custom-defined services, the framework provides many further services that you can use within your apps. The Reference Guide provides full details.

Two important such services are the repository/factory uses an injected RepositoryService (to persist or retrieve domain entities) and the FactoryService (to instantiate new domain objects).

It’s common to write your own domain service that wrap these framework-defined services, eg CustomerRepository or CustomerFactory. Generally these services are not visible in UI, and so would be @Service. You could if you wish also use Spring’s @Repository for your XxxRepository service, as a more descriptive alternative.

Example

The class diagram shows how you might implement a UI menu and REST API for a "customer" module:

example

The menu service:

Menu services provide actions to be rendered on the menu.

For the Web UI (Wicket viewer), each service’s actions appear as a collection of menu items of a named menu, and this menu is on one of the three menu bars provided by the Wicket viewer. Although these can be organised using annotations, it’s usually easier to use a file-based layout file (menubars.layout.xml).

For the REST API (Restful Objects viewer), all menu services are shown in the services representation.

import lombok.RequiredArgsConstructor;

@Named("customers.Customers")
@DomainService                                          (1)
@RequiredArgsConstructor(onConstructor_ = {@Inject} )   (2)
public class Customers {

    final CustomerRepository customerRepository;        (2)

    @Action(semantics = SemanticsOf.SAFE)
    public List<Customer> findByName(                   (3)
            final String name ) {
        return customerRepository.findByName(name);     (4)
    }

    @Action(semantics = SemanticsOf.NON_IDEMPOTENT)
    public Customer newCustomer(...) {
        return customerRepository.newCustomer(...);
    }

    @Action( semantics = SemanticsOf.SAFE,
             restrictTo = RestrictTo.PROTOTYPING )      (5)
    public List<Customer> listAll() {
        return customerRepository.listAll();
    }
}
1 Identify the class as a domain service, to render in the menu.
2 The CustomerRepository is injected through the constructor (Lombok creates the constructor for us).
3 Rendered in the UI as a "Find By Name" menu item underneath the "Customers" menu.
4 the action implementation delegates to the injected repository.
5 Prototype actions are rendered only in prototyping mode. A "list all" action such as this can be useful when exploring the domain with a small dataset.

The CustomerRepository would look something like:

import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

@Repository                                                     (1)
@RequiredArgsConstructor(onConstructor_ = {@Inject} )
public CustomerRepository {

    final RepositoryService repositoryService;

    public List<Customer> findByName(String name) {
        return repositoryService.allMatches(                    (2)
                Query.named(Customer.class, "findByName")
                    .withParameter("name", name));
    }

    public List<Customer> allCustomers() {                      (3)
        return repositoryService.allInstances(Customer.class);
    }

    // ...
}
1 Register as a service using Spring Boot’s @Repsitory annotation
2 uses injected RepositoryService to query
3 Returns all instances (useful for prototyping, probably not for production).

The CustomerFactory would look something like:

import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

@Service                                                        (1)
@RequiredArgsConstructor(onConstructor_ = {@Inject} )
public CustomerFactory {

    final FactoryService factoryService;
    final RepostiryoService factoryService;

    public Customer newCustomerNotPersisted(...) {
        Customer Customer =
            repositoryService.detachedEntity(Customer.class);   (2)
        ...
        return repositoryService.persistAndFlush(Customer);     (3)
    }

    // ...
}
1 Register as a service using Spring Boot’s @Service annotation
2 uses injected RepositoryService to instantiate a not-yet-persisted domain entity …​
3 ... and then save into the database a new Customer instance.

The CustomerMenu is part of the Causeway metamodel and its methods will be exposed as actions. The CustomerRepository and CustomerFactory are not part of the metamodel; there is no need to mark their methods as @Programmatic.

Separate services, or combined?

Whether you separate out menu services from repository services is to some extent a matter of style.

One perspective is that these two closely related domain services nevertheless have different responsibilities, and so could be kept separate.

An alternative perspective is that the duplication is just unnecessary boilerplate, and conflicts with the naked objects philosophy.

Object Management (CRUD)

One of the most common use cases for services is to create, read, update and delete domain entities, leveraging the framework-provided RepositoryService and FactoryService. This page shows some of the common idioms for achieving this.

Instantiating

Domain entities can be instantiated using the FactoryService provided by the framework. For example:

Customer customer = factoryService.detachedEntity(Customer.class);

The returned domain entity is not persistent and is unknown to the ORM; hence "detached".

When the framework instantiates the object, all services are injected into the framework, and an ObjectCreatedEvent lifecycle event will also be emitted.

You may prefer however for your domain entities to have regular constructor defining their minimum set of mandatory properties. For example:

public class Customer {

    public Customer(String reference, String firstName, String lastName) {
        // ...
    }

    // ...
}

In such cases, the domain object cannot be instantiated using FactoryService. Instead the ServiceInjector service can be used to inject services:

Customer customer = new Customer(reference, firstName, lastName);
factoryService.detachedEntity(customer);

If you prefer, this can be performed in one step:

Customer customer = factoryService.detachedEntity(
                        new Customer(reference, firstName, lastName));

Note though that this does not raise any lifecycle event.

Persisting

Once a domain entity has been instantiated and initialized, it can be persisted using the RepositoryService.

For example:

Customer customer = ...

repositoryService.persist(customer);

If using the no-arg form to instantiate the entity, then (to save having to inject the FactoryService as well), the RepositoryService can also be used to instantiate. This gives rise to this common idiom:

Customer customer = repositoryService.instantiate(Customer.class);
customer.setReference(reference);
customer.setFirstName(firstName);
customer.setLastName(lastName);
...
repositoryService.persist(customer);

On the other hand, there is often little need to inject services into the domain entity between its instantiation and persistence. If the domain entity has an N-arg constructor, then the code is often simpler:

Customer customer = repositoryService.persist(new Customer(reference, name, lastname));

Note that the persist() returns the object passed to it.

Eager Persistence

It’s worth being aware that the framework does not eagerly persist the object. Rather, it queues up an internal command structure representing the object persistence request. This is then executed either at the end of the transaction, or if a query is run, or if the internal queue is manually flushed using TransactionService's flush() method. Flushing also happens when a repository query is executed, so that the pending persist operation is performed first. Generally therefore the lazy persistence approach works well enough.

Nevertheless, if you want to ensure that the persist command is flushed immediately, you can use:

repositoryService.persistAndFlush(customer);

When an object is persisted the framework will emit ObjectPersistingEvent and ObjectPersistedEvent lifecycle events.

Persistence by Reachability (JDO)

If using JDO/DataNucleus, it is possible to configure ORM to automatically persist domain entities if they are associated with other already-persistent entities. This avoid the need to explicitly call "persist".

This is done using persistence-by-reachability configuration property:

application.properties
datanucleus.persistence-by-reachability-at-commit=true

One downside is that the code is arguably less easy to debug, and there may be performance implications.

Finding Objects

Retrieving domain entities depends on the ORM, though the RepositoryService can be used as an abstraction over either if required.

Finding Objects (JPA)

The easiest way to retrieve domain entities if using JPA is to leverage the capabilities of Spring Data.

For example, simply by declaring this interface:

public interface UserRepository extends Repository<User, Long> {

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

and Spring Data will create an implementation based on naming conventions. See the Spring Data documentation for further details.

It is also possible to declare JPQL queries , either on the repository method (using javax.persistence.Query) or on the entity (using javax.persistence.NamedQuery).

On the entity, declare a named query, eg:

@javax.persistence.Entity
@javax.persistence.NamedQueries({
    @javax.persistence.NamedQuery(          (1)
        name = "Customer.findByNameLike",   (2)
        query = "SELECT c " +               (3)
                "FROM Customer c " +        (4)
                "WHERE c.name LIKE :name"   (5)
    )
    })
...
public class Customer {
    // ...
}
1 There may be several @NamedQuery annotations, nested within a @NamedQueries annotation, defining queries using JPQL.
2 Defines the name of the query.
3 The definition of the query, using JPQL syntax.
4 The table name
5 The predicate, expressed using SQL syntax.

and in the corresponding repository, use RepositoryService:

import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor(onConstructor_ = {@Inject} )
public class CustomerRepository {

    private final RepositoryService repositoryService;

    public List<Customer> findByName(String name) {
        return repositoryService.allMatches(                            (1)
                Query.named(Customer.class, "Customer.findByNameLike")  (2)
                     .withParameter("name", "%" + name + "%");          (3)
    }

}
1 The RepositoryService is a generic facade over the ORM API.
2 Specifies the class that is annotated with @NamedQuery, along with the @NamedQuery#name attribute
3 The :name parameter in the query JPQL string, and its corresponding value

Finding Objects (JDO)

In the case of JDO/DataNucleus, it typically requires a JDOQL query defined on the domain entity, and a corresponding repository service for that domain entity type. This repository calls the framework-provided RepositoryService to actually submit the query.

For example:

@javax.jdo.annotations.PersistenceCapable
@javax.jdo.annotations.Queries({
    @javax.jdo.annotations.Query(                       (1)
        name = "findByName",                            (2)
        value = "SELECT "                               (3)
                + "FROM com.mydomain.myapp.Customer "   (4)
                + "WHERE name.indexOf(:name) >= 0 ")    (5)
})
...
public class Customer {
    // ...
}
1 There may be several @Query annotations, nested within a @Queries annotation, defining queries using JDOQL.
2 Defines the name of the query.
3 The definition of the query, using JDOQL syntax.
4 The fully-qualified class name. Must correspond to the class on which the annotation is defined (the framework checks this automatically on bootstrapping).
5 The predicate, expressed using Java syntax. In this particular query, is an implementation of a LIKE "name%" query.

and in the corresponding repository, use RepositoryService:

import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor(onConstructor_ = {@Inject} )
public class CustomerRepository {

    private final RepositoryService repositoryService;

    public List<Customer> findByName(String name) {
        return repositoryService.allMatches(                (1)
                Query.named(Customer.class, "findByName")   (2)
                     .withParameter("name", name);          (3)
    }

}
1 The RepositoryService is a generic facade over the ORM API.
2 Specifies the class that is annotated with @Query, along with the @Query#name attribute
3 The :name parameter in the query JDOQL string, and its corresponding value

Whenever a query is submitted, the framework will automatically "flush" any pending changes. This ensures that the database query runs against an up-to-date table so that all matching instances (with respect to the current transaction) are correctly retrieved.

When an object is loaded from the database the framework will emit ObjectLoadedEvent lifecycle event.

Type-safe queries

DataNucleus also supports type-safe queries; these can be executed using the JdoSupportService (JDO-specific) domain service.

See JdoSupportService for further details.

Updating Objects

There is no specific API to update a domain entity. Rather, the ORM (DataNucleus) automatically keeps track of the state of each object and will update the corresponding database rows when the transaction completes.

That said, it is possible to "flush" pending changes:

  • TransactionService acts at the Apache Causeway layer, and flushes any pending object persistence or object deletions

  • (if using JDO/DataNucleus), the JdoSupportService domain service can be used reach down to the underlying JDO API, and perform a flush of pending object updates also.

When an object is updated the framework will emit ObjectUpdatingEvent and ObjectUpdatedEvent lifecycle events.

Deleting Objects

Domain entities can be deleted using RepositoryService. For example:

Customer customer = ...
repositoryService.remove(customer);

It’s worth being aware that (as for persisting new entities) the framework does not eagerly delete the object. Rather, it queues up an internal command structure representing the object deletion request. This is then executed either at the end of the transaction, or if a query is run, or if the internal queue is manually flushed using TransactionService's flush() method.

Alternatively, you can use:

repositoryService.removeAndFlush(customer);

to eagerly perform the object deletion from the database.

When an object is deleted the framework will emit ObjectRemovingEvent lifecycle event.

Injecting services

Apache Causeway runs on top of Spring Boot, and uses Spring Boot for dependency injection, both the application’s own domain services and also the many additional services defined by the framework (such as RepositoryService).

Since this is a core capability of Spring, it’s worth checking out Spring’s documentation on the topic.

Injection is requested using the JEE @javax.inject.Inject annotation. This is described in Spring’s documentation, using JSR330 standard annotations.

It is also possible to use Spring’s own @Autowired annotation. Since the two annotations are for the most part equivalent, we recommend using the JEE standard.

However, not only does Apache Causeway use Spring to autowire domain services into other services, the framework also ensures that services are injected into any domain object (eg entity, view model, mixins, fixture script, specification etc). This is key enabler to place functionality in the "right place", eg in a domain entity/view model itself, or in a mixin.

There are three ways in which to inject the domain services:

  • constructor injection (further discussion in the Spring documentation, here)

    This is recommended approach, but note that it is only supported for domain services and (non-JAXB) view models, but not for entities, mixins or value types.

  • setter injection (further discussion here)

  • field injection

Whether you use setter or field injection for domain objects etc is a matter of style. Generally field injection is somewhat frowned up.

Constructor Injection.

As noted above, constructor injection is only available for domain services. For example:

CustomerRepository.java
import org.springframework.data.repository.Repository;      (1)

@Repository
public class CustomerRepository {
    private final RepositoryService repositoryService;
    public CustomerRepository(
                final RepositoryService repositoryService) {
        this.repositoryService = repositoryService;
    }
    // ...
}
1 indicates this is a repository service.

If you wish, Project Lombok can be used to remove some of the boilerplate:

CustomerRepository.java
import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor(onConstructor_ = {@Inject} )   (1)
public class CustomerRepository {
    private final RepositoryService repositoryService;
    // ...
}
1 Generates a constructor for all final fields.

If the layering between services is well defined, as in the above example (application CustomerRepository depends upon framework RepositoryService), then constructor injection should work out.

Be aware though that Spring does not support cyclic dependencies with constructor injection. In such cases, either use setter injection, or alternatively inject Provider<ServiceType> rather than ServiceType, thereby allowing the dependent bean to be lazily created.

For more on this topic, see Spring’s documentation on the Dependency Resolution Process, "Circular dependencies" sidebar.

Setter and Field Injection

Setter or field injection must be used all objects other than domain services. For example, setter injection is:

import javax.inject.Inject;

public class Customer {
    ...
    OrderRepository orderRepository;
    @Inject                                                 (1)
    public void setOrderRepository(orderRepository) {
        this.orderRepository = orderRepository;
    }
}
1 The framework injects the domain service into the entity, before any further interactions with it.

It’s not necessary for the visibility to be public, so it should be as restrictive as possible. In many cases, default visibility will work (assuming unit tests that mock the dependency are in the same package).

Some of the boilerplate can be removed using Project Lombok:

import javax.inject.Inject;
import lombok.Setter;

public class Customer {
    ...
    @Setter(value= AccessLevel.PACKAGE, onMethod_ = {Inject.class}) (1)
    OrderRepository orderRepository;
}
1 Generates a package-level setter, annotated with @Inject

If you want to use field injection, then this is simply:

import javax.inject.Inject;

public class Customer {
    ...
    @Inject OrderRepository orderRepository;
}

... and live with or disable any IDE warnings relating to field injection.

Using default visibility here still allows the field to be mocked out within unit tests (if placed in the same package as the code under test).

Multiple Implementations

If there is more than one implementation of the service, then a specific implementation can be requested using either Spring’s @Primary annotation (further discussion here) or Spring’s Qualifier annotation (further discussion here).

All of the domain services provided by Apache Causeway' are annotated with @Qualifier to enable this.

Injecting Lists of Services

It’s also possible to inject a list of services:

import javax.inject.Inject;

public class DocumentService {
    ...
    @Inject List<PaperclipFactory> paperclipFactories;
}

These will be in the order as defined by the @javax.annotation.Priority annotation.

This pattern can be useful when implementing the chain of responsibility design pattern, that is, looking for the first implementation that can handle a request.

It is also useful to "broadcast" or fan out an implementation. For example, the framework defines the ExecutionSubscriber SPI, which is used to publish Interaction Executions to external systems. The framework provides a simple logging implementation, which will always be called. All other implementations available will also be called.

Scoped Services

By default all domain services are application-scoped, in other words singletons. Such domain services are required to be thread-safe, usually satisfied by being intrinsically stateless.

Sometimes though a service’s lifetime is applicable only to a single (http) request. The framework has a number of such services, including a Scratchpad service (to share adhoc data between methods), and QueryResultsCache, which as its name suggests will cache query results. Such services do hold state, but that state is scoped per (possibly concurrent) request and should be removed afterwards.

The requirement for request-scoped services is supported using Apache Causeway' own @InteractionScope annotation (named because a short-lived CausewaySession is created for each request). This is used by the framework services and can also be used for user-defined services.

For example:

@Service
@InteractionScope
public class MyService {
    ...
    public void doSomething() { ... }
}

Unlike application-scoped services, these request-scoped services must be injected using a slightly different idiom (borrowed from CDI), using a javax.inject.Provider. For example:

import javax.inject.Provider;

public class SomeClient {
    ...
    @Inject Provider<MyService> myServiceProvider;  (1)

    public void someMethod() {
        myServiceProvider.get()                     (2)
                         .doSomething();
}
1 Inject using Provider
2 Obtain an instance using Provider#get()

As mentioned above, the QueryResultsCache (used for performance caching) is also scoped. To use that service, the idiom would be:

import javax.inject.Inject;
import javax.inject.Provider;

public class Customer {
    ...
    @Inject OrderRepository orderRepository;
    @Inject Provider<QueryResultsCache> queryResultsCacheProvider;  (1)

    public List<Order> getOrders() {
        Customer customer = this;
        return queryResultsCacheProvider
                .get()                                              (2)
                .execute(
                    () -> orderRepository.findByCustomer(customer),
                    Customer.class, "getOrders",
                    customer)
        );
}
1 inject a Provider for the service, not directly
2 Get the cache from the provider

If you accidentally inject the service directly (without being wrapped in Provider), then the framework will detect this and fail-fast.

Overriding/Replacing Services

Apache Causeway runs on top of Spring Boot, and relies on Spring Boot for dependency injection using @javax.inject.Inject. The @javax.annotation.Priority annotation is used to prioritize multiple service implementations. This allows any framework-provided domain service to be replaced by a user-defined one if required, simply by assigning it with an earlier precedence (= higher priority).

If this is done, then you will probably also want to implement your replacement using @org.springframework.context.annotation.Primary, to resolve the ambiguity of there being more than one implementation of the service on the classpath.

It’s also possible to inject a List<SomeService>, in which case the list will be ordered with according to their @Priority, lowest values first.

Initialization

Sometimes a domain service needs to perform initialization logic before it is ready to be used.

In many cases, such initialization can be performed within the constructor. If the initialization has dependencies, then these can be injected using standard constructor injection.

Alternatively, initialization can be moved into a @PostConstruct lifecycle callback. Shutdown is similar; the framework will call any method annotated with javax.annotation.PreDestroy.

If a domain service needs to wait until the framework is fully initialized though, it should register for the MetamodelEvent that is emitted by the framework itself. One example is if the service is to seed some reference data:

SimpleModule.java
import org.springframework.context.annotation.Configuration

@Service
@Priority(99)
@RequiredConstructor
public class SeedCountryRefData {

    final InteractionService interactionService;
    final TransactionService transactionService;
    final FixtureScripts fixtureScripts;

    @EventListener(MetamodelEvent.class)
    public void init(MetamodelEvent ev) {                       (1)
        if (ev.isPostMetamodel()) {
            interactionService.runAnonymous(() -> {
                transactionService.runWithinCurrentTransactionElseCreateNew(
                    () -> {
                        fixtureScripts.runFixtureScript(
                            new SeedCountryFixture(), null);    (2)
                });
            });
        }
    }
}
1 subscribe to the framework-emitted MetamodelEvent
2 uses FixtureScripts to seed the data using a fixture script.

Configuration

Spring provides numerous mechanisms to configure domain services, both in terms of binding or passing in the configuration property to the service, and in terms of setting the value within some sort of configuration file.

The mechanism prefered by Apache Causeway itself, and which you are free to use for your own services, is the type-safe ConfigurationProperties, whereby the configuration properties are expressed in a series of nested static classes.

The simpleapp starter app includes an example:

import org.springframework.validation.annotation.Validated;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("app.simple-module")
@lombok.Data
@Validated
public static class Configuration {
    private final Types types = new Types();
    @lombok.Data
    public static class Types {
        private final Name name = new Name();
        @lombok.Data
        public static class Name {
            private final Validation validation = new Validation();
            @lombok.Data
            public static class Validation {
                private char[] prohibitedCharacters =
                    "!&%$".toCharArray();
                private String message =
                    "Character '{character}' is not allowed";
            }
        }
    }
}

This configuration property can be injected, like any other component, and makes the configuration value available in a type-safe fashion:

val prohibitedCharacters =
  config.getTypes().getName().getValidation().getProhibitedCharacters();

For this configuration property service to be discovered and managed by Spring, we need to use the EnableConfigurationProperties annotation. This normally would reside on the owning module (discussed in more detail later):

SimpleModule.java
import org.springframework.context.annotation.Configuration

@Configuration
// ...
@EnableConfigurationProperties({
        SimpleModule.Configuration.class,
})
public class SimpleModule /* ... */ {
    // ...
}

These configuration properties can then be specified using either Spring’s application.yml or application.properties. For example:

application.yml
app:
  simple-module:
    types:
      name:
        validation:
          message: "'{character}' is invalid."
          prohibited-characters: "&%$"

Moreover, Spring is able to configure the IDE so that these configuration values can be specified using code completion. All that is required is this dependency:

pom.xml
<!-- IDE support  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>