I have to following requirements :
A player in an FPS Shooter game have health points, where he can take damage and regain health.
One of the ways to regain health is 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) failed (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 understanding some things :
- Where should the powerup api call be managed, in the domain service or in the application service ?
- Is the call a business logic or an infrastructure ?
- Should domain services be transaction aware ?
- 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;
}
}
}
}