-2

So, while developing an app, I have to use event sourcing to track down all changes to model. The app itself is made using spring framework. The problem I encountered: for example, user A sends a command to delete an entity and it takes 1 second to complete this task. User B, at the same time, sends a request to modify, for example, an entity name and it takes 2 seconds to do so. So my program finishes deleting this entity (persisting an event that says this entity is deleted), and after it another event is persisted for the same entity, that says that we just modified its name. But no actions are allowed with deleted entities. Boom, we just broke the app logic. It seems to me, that I have to put methods that write to database in synchronized blocks, but is there are any other way to handle this issue? Like, I dunno, queuing events? The application is not huge, and not a lot of requests are expected, so users can wait for its request turn in the queue (of course I can return 202 HTTP Status Code to him, but like I said, requests are not resource heavy and there wont be a lot of them, so its unnecessary). So what is the best way for me to use here?

EDIT: Added code to illustrate the problem. Is using synchronized in this case is a good practice or there are other choices?

@RestController
@RequestMapping("/api/test")
public class TestController {

    @Autowired
    private TestCommandService testCommandService;

    @RequestMapping(value = "/api/test/update", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void update(TestUpdateCommand command) {
        testCommandService.update(command);
    }

    @RequestMapping(value = "/api/test/delete", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void delete(Long id) {
        testCommandService.delete(id);
    }

}

public class TestUpdateCommand {
    private Long id;
    private String name;

    public TestUpdateCommand() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public interface TestCommandService {

    void delete(Long id);
    void update(TestRegisterCommand command);

}

@Service
public class TestCommandServiceImpl implements TestCommandService {

    @Autowired
    TestEventRepository testEventRepository;

    @Override
    @Transactional
    public void delete(Long id) {
        synchronized (TestEvent.class) {
            //do logic, check if data is valid from the domain point of view. Logic is also in synchronized block
            DeleteTestEvent event = new DeleteTestEvent();
            event.setId(id);
            testEventRepository.save(event);
        }
    }

    @Override
    @Transactional
    public void update(TestUpdateCommand command) {
        synchronized (TestEvent.class) {
            //do logic, check if data is valid from the domain point of view. Logic is also in synchronized block
            UpdateTestEvent event = new DeleteTestEvent();
            event.setId(command.getId());
            event.setName(command.getName());
            testEventRepository.save(event);
        }
    }
}

@Entity
public abstract class TestEvent {
    @Id
    private Long id;

    public Event() {
    }

    public Event(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

@Entity
public class DeleteTestEvent extends TestEvent {

}

@Entity
public class UpdateTestEvent extends TestEvent {
    private String name;

    public UpdateTestEvent() {
    }

    public UpdateTestEvent(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public interface TestEventRepository extends JpaRepository<TestEvent, Long>{

}

1 Answers1

0

Make sure you read Don't Delete -- Just Don't by Udi Dahan.

I have to put methods that write to database in synchronized blocks, but is there are any other way to handle this issue?

Yes, but you have to be careful about identifying what the issue is...

In the simple version; as you have discovered, allowing multiple sources of "truth" can introduce a conflict. Synchronization blocks is one answer, but scaling synchronization is challenging.

Another approach is to use a "compare and swap approach" -- each of your writers loads the "current" copy of the state, calculates changes, and then swaps the new state for the "current" state. Imagine two writers, one trying to change state:A to state:B, and one trying to change state:A to state:C. If the first save wins the race, then the second save fails, because (A->C) isn't a legal write when the current state is B. The second writer needs to start over.

(If you are familiar with "conditional PUT" from HTTP, this is the same idea).

At a more advanced level, the requirement that the behavior of your system depends on the order that messages arrive is suspicious: see Udi Dahan's Race Conditions Don't Exist. Why is it wrong to change something after deleting it?

You might be interested in Martin Kleppmann's work on conflict resolution for eventual consistency. He specifically discusses examples where one writer edits an element that another writer deletes.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91