DynamoDB enhanced client silently fails when necessary extensions are not installed + exposes no list of installed extensions
Describe the bug
I learned the hard way that if the VersionedRecordExtension is not installed on my instance of DynamoDbEnhancedClient, the client silently ignores DynamoDbVersionAttribute annotations, stops performing optimistic locking, and writes null versions to the database. There is no compile time or run time check for missing extensions.
Regression Issue
- [ ] Select this option if this issue appears to be a regression.
Expected Behavior
I expected the client to provide a fast-fail mechanism of some kind if necessary extensions are not installed to support the annotations used in a DynamoDbBean entity definition. At the very least, it would be ideal if the DynamoDbEnhancedClient instances exposed a list of installed extensions so callers could implement their own fast-fail logic.
Current Behavior
The DynamoDbEnhancedClient quietly ignores annotations that are not supported by its list of installed extensions.
Reproduction Steps
The following code snippets can be used to write a record to Dynamo that should have a version field populated with the value 1. Instead, it will have no value for this field.
Entity definition (Kotlin)
@DynamoDbBean
data class MyEntity(
@get:DynamoDbPartitionKey
@get:DynamoDbAttribute("pk")
var pk: String = "",
// Sort Key, should always be "LOAN" unless we introduce new types of records,
// which would probably be represented by different classes.
@get:DynamoDbSortKey
@get:DynamoDbAttribute("sk")
var recordType: String = "",
@get:DynamoDbVersionAttribute
@get:DynamoDbAttribute("version")
var version: Int? = null,
)
Client setup (Java):
var client = DynamoDbEnhancedClient.builder()
.dynamoDbClient(DynamoDbClient.builder().build())
.extensions(List.of()) // Some list of extensions that causes record versioning extension not to be installed
.build();
Usage of table (Kotlin):
val table = client.table("my-table", TableSchema.fromBean(MyEntity::class.java))
val record = MyEntity(
pk = "some-pk",
sk = "some-sk"
)
table.putItem(PutItemEnhancedRequest.builder(MyEntity::class.java)
.item(record)
.build())
Possible Solution
At a minimum, if the enhanced client object could provide a public list of extensions installed, callers could add start-up logic in their applications to assert that expected extensions are there.
Additional Information/Context
No response
AWS Java SDK version used
2.31.54
JDK version used
21.0.4
Operating System and version
MacBook Pro, Apple M2 Max, Sequoia 15.5
Hi @eliana404,
Thanks for the report. I think there might be a misunderstanding about the expected behavior here.
When you explicitly set .extensions(List.of()), you're telling the Enhanced Client to use no extensions at all. This removes the default extensions that are normally included, including VersionedRecordExtension which handles @DynamoDbVersionAttribute.
From an SDK perspective, the client is working as designed. If you don't specify extensions, the defaults are automatically included. By explicitly providing an empty list, you're overriding this behavior.
Can you tell us why are you passing an empty list to begin with? is that being set implicitly somehow?
Thanks, Ran~
Hi @eliana404,
Thanks for the report. I think there might be a misunderstanding about the expected behavior here.
When you explicitly set
.extensions(List.of()), you're telling the Enhanced Client to use no extensions at all. This removes the default extensions that are normally included, includingVersionedRecordExtensionwhich handles@DynamoDbVersionAttribute.From an SDK perspective, the client is working as designed. If you don't specify extensions, the defaults are automatically included. By explicitly providing an empty list, you're overriding this behavior.
Can you tell us why are you passing an empty list to begin with? is that being set implicitly somehow?
Thanks, Ran~
Hello Ran,
Perhaps my question wasn't clear. The issue is not about the behavior of the .extensions() installation function. See my comment next to that code. The point is that any list of extensions which doesn't include the one my Java bean class references can be installed without resulting in any kind of compile time or run time error (I provided an empty list because it was simple and it doesn't really matter what I put there to illustrate the point I'm trying to make, as long as the extension of interest isn't installed). I cannot even write my own custom code to force a runtime assertion error because the client object doesn't expose which extensions are currently installed. For an app using dependency injection, this can easily be disastrous. Someone refactors the injection modules, unknowingly binds a new instance of the enhanced client that doesn't have the same extensions installed as before, and suddenly the app is quietly corrupting data in DynamoDb because it doesn't know about record versioning anymore. The library currently provides no way to defend against this scenario. A simple exposed list of which modules are installed on an instance of the client would be a good enough quick fix in lieu of a more robust solution.
To use an analogy, suppose we live in a world where I like to write web servers in some programming language yolo, with package manager ypm. Suppose ypm provides a config file which allows me to specify which dependencies I want, but it doesn't let me pin the version and just gives me a random major version of each library every time I build the server, and there is no way at runtime to tell which versions I'm using. Working with this client in an environment with dependency injection is sort of like that. My application depends on specific extensions being installed, but there is absolutely no way to know if the client being passed to me knows about those extensions. It's very much like being given a random major version of a library for which I can't guarantee that certain features are supported. In the analogy, the library would at least be likely to throw an error if I tried to access a missing function. My request is that this client does the same, or at least exposes somewhere what extensions are installed so I can fast fail.
I agree, if I use an annotation it would be nice to have a mechanism detect this and throw an exception if I didn't add extensions