1

I have the following requirements :

  • A player in an FPS Shooter game has health points, where he can take damage and regain health.

  • One of the ways to regain health is by using a Health Booster that has been purchased from the store.

  • One of the packages is a SmallHealthBooster that increase the health by 10 (or to max health if health + 10 > max health).

  • Every time the user applies SmallHealthBooster he shall receive 5 Reputation Points.

  • To consume powerups I use a third-party api (another microservice).
    In case the transaction (operation) fails (persistence or even microservice communication error) we want to revert the powerup consumption (return it).

  • Obviously, I want the operation to be transactional, but I have a hard to understand some things :

    1. Where should the powerup api call be managed, in the domain service or in the application service ?
    2. Is the call a business logic or an infrastructure ?
    3. Should domain services be transaction aware ?
    4. How can I revert (or rollback) an api called on a failing transaction ?

I know that the domain model (and service) don't actually care where it gets its powerup from,
like this answer said (A Domain Object is not responsible for providing information outside of it's own context.), but it feels to me that call need happen inside the domain service...

Here is some code :

Calling the api from domain service

public class PlayerHealthDomainServices
{
    private readonly IPowerupsApi _powerupsApi;

    public async Task UseSimpleHealthBooster(Player player)
    {
        //Maybe check if were in a transaction ?
        //if(Transaction.Current != null) throw new InvalidOperationException();
        var powerup = (PurchasedPowerup)null;

        try
        {
            powerup = await _powerupsApi.ConsumeAsync(KnownBoosters.SmallHealthBooster, player.Id);
            //TODO: Persist powerup
            player.AddHealth(10);
            player.Reputation += 5;
        }
        catch
        {
            if (powerup != null) await _powerupsApi.RevertAsync(powerup);
            //TODO: Rollback powerup persistance
            throw;
        }
    }
}

public class PlayerHealthApplicationServices
{
    private readonly IPlayersRepository _players;
    private readonly PlayerHealthDomainServices _service;

    public async Task HandleUseBooster(Guid id)
    {
        var player = await _players.Get(id);

        using (var trx = new TransactionScope())
        {
            await _service.UseSimpleHealthBooster(player);
            await _players.Save(player);
            trx.Complete();
        }
    }
}

Calling the api from application service

public class PlayerHealthDomainServices2
{
    private readonly IPowerupsApi _powerupsApi;

    public async Task UseSmallHealthBooster(PurchasedPowerup confiramtion, Player player)
    {
        //TODO: Persist the confiramtion
        player.AddHealth(10);
        player.Reputation += 5;
        
    }
}


public class PlayerHealthApplicationServices2
{
    private readonly IPlayersRepository _players;
    private readonly PlayerHealthDomainServices2 _service;
    private readonly IPowerupsApi _powerupsApi;

    public async Task HandleUseBooster(Guid id)
    {

        var player = await _players.Get(id);
        var powerup = (PurchasedPowerup)null;

        using (var trx = new TransactionScope())
        {
            try
            {

                powerup = await _powerupsApi.ConsumeAsync(KnownBoosters.SmallHealthBooster, player.Id);

                await _service.UseSmallHealthBooster(powerup, player);
                await _players.Save(player);

                trx.Complete();
            }
            catch
            {
                if (powerup != null) await _powerupsApi.RevertAsync(powerup);
                //TODO: Rollback powerup confirmation persisatnce ??
                throw;
            }
        }
    }
}
2
  • 2
    This question currently includes multiple questions in one. It should focus on one problem only. I’ve answered the question listed under bullet point 1. Please edit your post and create new questions that focus on a single problem. Of course after you did a search on this site to confirm the question hasn’t been asked before.
    – Rik D
    Commented Mar 19, 2023 at 18:49
  • 1
    The title of this question is not grammatical. Please clarify.
    – JimmyJames
    Commented Apr 18, 2023 at 20:40

1 Answer 1

1

I prefer my domain layer to be pure and not depend on any out-of-process service, either directly or via an interface. This makes the model easy to test without the need for mocks. So I would always make such a call in the application layer and invoke the domain logic with the values retrieved from the external service.

On a side note, I would encapsulate the behavior to update the player health in the Player aggregate instead of a domain service. Player.UseSmallHealthBooster makes a lot of sense to me. Avoid setting values in an aggregate if you can; exposing the setter via an AddHealth method is only slightly better than exposing it directly.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.