How do you model an operation saving an image using DDD?
Asked Answered
H

3

5

Basically, an user wants to change its profile picture. The web server receives a posted image against /user/35435/profile/picture, so the data needs to be saved, and the LastModification property of the profile object updated.

Images are not stored locally in the web server, they need to be uploaded somewhere else (ie: cloud storage).

At the moment, every operation is represented by either a Command or a Query. Commands run in an ambient transaction (like a SQL transaction). The image upload operation is not transactional, but a compensating action can be done in case of error (ie: removing the image if the DB operation fails).

On a naive implementation, a command containing both current date and the image data is created. A command handler is executed, it loads the ProfileAggregate aggregate, and ProfileAggregate.updateProfilePicture(imageUploader, image, currentDate) is executed passing the imageUploader domain service as parameter. Inside the method, the image is uploaded and the profile updated. The command handler saves changes in the DB and returns.

I do not like the fact that a transaction is held during the image upload. I do not like either doing non-domain-model operations (like uploading the image) from within an aggregate (even if the aggregate is calling somewhere else).

Should this interaction modeled like two independent commands that are executed in serial fashion, or it is OK to put anything inside an aggregate as long there is no dependencies in concrete implementations.

Herbartian answered 5/6, 2018 at 14:29 Comment(2)
KISS: upload the image before you load the Aggregate and sent it only image metadata and storageId (without the actual image data)Alcaic
Commands are meant to orchestrate logic implemented in the domain. This looks like orchestration of the image upload and the aggregate update. I agree with Constantin above, though I would generate an image storageID first, launch the upload async, then update the aggregate. The aggregate should be able to handle missing images if something goes awry with the upload.Ondine
P
5

Your command handler is something that's probably in the "Application Layer" of your architecture. So, considering that, you receive a command and you orchestrate your domain / other services to satisfy it.

That said, I don't really like two different commands implementation as from the client's point of view they're only doing one action. You could create a proxy command (single command) and on its handler generate two commands (one for handling the aggregate, the other to handle the upload) but this approach will make your API/Model less intuitive in the future.

I also don't like bundling the upload part of the operation inside the Profile aggregate as it doesn't seem like one of its responsibilities at all.

What I will suggest here is the following approach:

  • The Profile aggregate should be responsible for accepting a picture. So, its operation updateProfilePicture will simply evaluate picture metadata (maybe you have a rule that the picture must have a certain size or be validated against some sort of algorithm to try to find nudity, this kind of stuff) and the internal state of target Profile to allow the action to occur and to update its internal state with lastUpdated property or something.

  • The ImageUploadService will provide you with standalone functionality to receive the image and store it accordingly.

  • Both these operations will happen independently and will be eventually consistent through messaging.

As such, your command handler will:

  • Load the Profile aggregate
  • Call the updateProfilePicture (which will abort if an invalid state is reached). Here, you might want to leave the Profile aggregate in a PendingUpload State (if you want to take the pessimistic approach to the upload) or just assume the upload will usually succeed (taking the optimistic approach) and take compensating measures when it fails.
  • Call the ImageUploadService directly and asynchronously (fire and forget)
  • Commit changes to Profile once the upload is on its way

When ImageUploadService finished receiving the image, it will fire a message to the Profile domain reporting success/failure of the upload. Then:

  • If you took the pessimistic approach, a Success would take the Profile aggregate out of its pending state.
  • If you took the optimistic approach, no handling needed in a Success.
  • In any of the cases, an upload failure should be handled by rolling back the Profile aggregate to the pre-update state of the picture.
Parts answered 5/6, 2018 at 15:28 Comment(1)
@Pedro_goes,I've gotten what u suggested but my question is how should I transfer file(itself) to application layer. Shall I send it as "bytes" or "FileStream" or "HttpPostedFile" or something else?Horseshit
A
6

The main question that will help you is what should the system do when the image could not be uploaded. I think the answer is that you want the user to receive an error and the system (the Profile) should remain unchanged.

From the Domain point of view, the actual image uploading/storing process is irrelevant. This action is in fact part of the infrastructure and should be orchestrated by the Application layer before the command is sent to the Domain layer (to the Profile Aggregate). In this way, if the upload fail for whatever reason, the command will not be sent.

The image uploading could be done synchronously or asynchronously; it depends on your platform/programming language/etc and it's irrelevant. The point is that you send the command to the Aggregate only after the image upload is successful. The command should include the ImageStorageId and possibly some metadata (like filename, filesize, imagesize etc).

Alcaic answered 8/6, 2018 at 7:8 Comment(3)
what would happen if the saving into the database fails, would the image remain orphaned? how should i trigger that compensating action that would delete it?Herbartian
@Herbartian Yes, you need a way to detect orphaned images but this should not be impossible. You may somehow log the databases exceptions or look at the latest image uploads and correlate them with the profiles.Alcaic
In case of failure domain fires an event with reference to failed image. Application handles this event and deletes already uploaded file or does some other rollback actionExaltation
P
5

Your command handler is something that's probably in the "Application Layer" of your architecture. So, considering that, you receive a command and you orchestrate your domain / other services to satisfy it.

That said, I don't really like two different commands implementation as from the client's point of view they're only doing one action. You could create a proxy command (single command) and on its handler generate two commands (one for handling the aggregate, the other to handle the upload) but this approach will make your API/Model less intuitive in the future.

I also don't like bundling the upload part of the operation inside the Profile aggregate as it doesn't seem like one of its responsibilities at all.

What I will suggest here is the following approach:

  • The Profile aggregate should be responsible for accepting a picture. So, its operation updateProfilePicture will simply evaluate picture metadata (maybe you have a rule that the picture must have a certain size or be validated against some sort of algorithm to try to find nudity, this kind of stuff) and the internal state of target Profile to allow the action to occur and to update its internal state with lastUpdated property or something.

  • The ImageUploadService will provide you with standalone functionality to receive the image and store it accordingly.

  • Both these operations will happen independently and will be eventually consistent through messaging.

As such, your command handler will:

  • Load the Profile aggregate
  • Call the updateProfilePicture (which will abort if an invalid state is reached). Here, you might want to leave the Profile aggregate in a PendingUpload State (if you want to take the pessimistic approach to the upload) or just assume the upload will usually succeed (taking the optimistic approach) and take compensating measures when it fails.
  • Call the ImageUploadService directly and asynchronously (fire and forget)
  • Commit changes to Profile once the upload is on its way

When ImageUploadService finished receiving the image, it will fire a message to the Profile domain reporting success/failure of the upload. Then:

  • If you took the pessimistic approach, a Success would take the Profile aggregate out of its pending state.
  • If you took the optimistic approach, no handling needed in a Success.
  • In any of the cases, an upload failure should be handled by rolling back the Profile aggregate to the pre-update state of the picture.
Parts answered 5/6, 2018 at 15:28 Comment(1)
@Pedro_goes,I've gotten what u suggested but my question is how should I transfer file(itself) to application layer. Shall I send it as "bytes" or "FileStream" or "HttpPostedFile" or something else?Horseshit
E
0

I'm currently sticking with handling file uploads before running the domain command.

Unless domain model requires to work with the image (or some other) file directly, it seems reasonable to keep all file-based operation (including uploads and cropping) away from it. So if you do not analyze data in the image in your model (like, I dont know, face detection?), and just need some abstract profile image, you pass metadata object to the model.

In case of failure in domain model it dispatches an event, which application handles and does some rollback logic.

Exaltation answered 15/6, 2021 at 6:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.