- Configuration should be loaded and validated at extension initialization so that issues are reported immediately. Do not lazy-load configuration unless it is required to do so.
- Settings can be pulled from the extension context and placed into configuration objects, which are passed to services via their constructor.
- Service configuration requirements should always be explicit; as a general rule, do not pass a single configuration
object with many values to multiple services.
For example, see
HttpFunctionConfiguration.java
. - Annotate configuration keys with
@Setting
so that they may be tracked.
- Do not throw checked exceptions; use unchecked exceptions. If an unchecked exception type needs to be defined, inherit from EdcException.
- Do not throw exceptions to signal a validation error; report the error (preferably collated) and return an error response.
- Throw an unchecked exception if something unexpected happens (e.g. a backing store connection is down after a number
of retries). Note that validation errors are expected.
For example, see
Result.java
. - Only throw an exception when there is no remediation possible, i.e. the exception is fatal. Do not throw an exception if an operation can be retried.
- Avoid layers of indirection when they are not needed (e.g. "pass-through methods").
- Avoid needlessly wrapping objects, especially primitive datatypes.
- Use
var
instead of explicit types (helps with clarity) - Avoid
final
in method args and local variables - Use
final
in field declarations - Avoid
static
fields except in constants or when absolutely necessary. (you should be able to provide a reason). - Use interfaces to define shared constants
- Use "minimally required types" (or "smallest possible API"), e.g. use
ObjectMapper
instead ofTypeManager
, or use aString
instead of a more complex object containing the String, etc. - Use either
public
members, which are documented and tested, orprivate
members. - Avoid package-private members, especially if only needed for testing
- Avoid
protected
members unless they're intended to be overridden. - Use package-private classes if they're not needed outside the package, e.g. implementation classes
- Avoid using
enum
s for anything other than named integer enumerations. - Avoid using static classes as much as possible. Exceptions to this are helper functions and test utils, etc. as well as static inner classes.
- Use only camel case and no prefixes for naming.
- Avoid unnecessary
this.
except when it is necessary e.g. when there is a name overlap - Use static imports, as long as code readability and comprehension is not impacted. For example,
- use
assertThat(...)
instead ofAssertions.assertThat(...)
- use
format("...",arg1)
instead ofString.format(...)
, but - avoid
of(item1, item2).map(it -> it.someOperation)...
instead ofStream.of(item1, item2)
. Also, avoid static imports if two static methods with the same name would be imported from different classes
- use
- Avoid
Optional
as method return type or method argument, except when designing a fluent API. Usenull
in signatures. - Avoid cryptic variable names, especially in long methods. Instead, try to write them out, at least to a reasonable extent.
- All handlers and services should have dedicated unit tests with mocks used for dependencies.
- Prefer unit tests over all other test types: unit > integration/component > e2e
- When appropriate, prefer composing services via the constructor so that dependencies can be mocked as opposed to instantiating dependencies directly.
- Use classes with static test functions to provide common helper methods, e.g. to instantiate an object.
- Use
[METHOD]_when[CONDITION]_should[EXPECTATION]
as naming template for test methods, e.g.verifyInput_whenNull_shouldThrowNpe()
as opposed totestInputNull()
-
Use the
Builder
pattern when:- there are any number of optional constructor args
- there are more than 3 constructor args
- inheriting from an object that fulfills any of the above. In this case use derived builders as well.
-
Although serializability is not the reason we use the builder pattern, it is a strong indication that a builder should be used.
-
Builders should be named just
Builder
and be static nested classes. -
Create a
public static Builder newInstance(){...}
method to instantiate the builder -
Builders have non-public constructors
-
Use single-field builders: a
Builder
instantiates the object it builds in its constructor, and sets the properties in its builder methods. Thebuild()
method then only performs verification (optional) and returns the instance. -
Use
private
constructors for the objects that the builder builds. -
If there is a builder for an object, use it to deserialize an object, i.e. put Jackson annotations such as
JsonCreator
and@JsonBuilder
on builders. -
Note that the motivation behind use of builders is not for immutability (although that may be good in certain circumstances). Rather, it is to make code less error-prone and simpler given the lack of named arguments and optional parameters in Java.
- Only store secrets in the
Vault
and do not hold them in objects that may be persisted to other stores. - Do not log secrets or sensitive information.
- Extension modules contribute a feature to the runtime such as a service.
- SPI modules define extensibility points in the runtime. There is a core SPI module that defines extensibility for essential runtime features. There are other SPI modules that define extensibility points for optional features such as IDS.
- Libraries are utility modules that provide classes which may be used by other modules. They do not directly contribute features to the runtime.
- An SPI module may only reference other SPI modules and library modules.
- An Extension module may only reference other SPI modules and library modules.
- A library module may only reference other library modules.
- There should only be a root
gradle.properties
that contains build variables. Do not create separategradle.properties
files in a module. - For external dependencies, do not reference the version directly. Instead, use the version catalog feature.
- In certain situations,
null
may need to be returned from a method, passed as a parameter, or set on a field. Only useOptional
if a method is part of a fluent API. Since the runtime will rarely require this, the project standard is to use theorg.jetbrains.annotations.Nullable
andorg.jetbrains.annotations.NotNull
annotations.
TypeManager
is the component responsible for json ser/des, you can also use theObjectMapper
inside it, but there should be no otherObjectMapper
instance.
- A single implementor of an interface should be named
<interface name>Impl
. - An implementor who are meant to be the default implementation for an interface but other are/can be defined used instead.
- Services are instrumented for collecting essential metrics, in particular instances
of
ExecutorService
.
- Always close explicitly
Stream
objects that are returned by a service/store, since they could carry a connection, and otherwise it will leak.