Q:

businesscode-enforced constraint for data persistence

I am currently implementing a JavaEE-based application, which is roughly structured like this:
RestService
├── BusinessService
    ├── ServiceDAO

The webservice takes requests and forwards them to the actual business service, which then performs multiple operations through a DAO. The important part is, that the BusinessService has to check the data against existing records using a constraint that can only be expressed through business code, not by SQL constraints (we do not want to implement complex database triggers or stored procedures). In this case that constraint is that no two datasets may have overlapping validity.
For a minimal scenario, the code looks like this:
@Table
public class DataEntity {
    @Id
    public int id;

    @Column("validFrom")
    public Instant validFrom;

    @Column("validTo")
    public Instant validTo;
}

@Path("/rest-service-path")
public class RestService {
    @Inject
    private BusinessService businessService;

    @POST
    @Path("add")
    public void addData(DataEntity data) {
        businessService.addData(data);
    }
}

@Stateless
public class BusinessService {
    @Inject
    private ServiceDAO serviceDAO;

    public void addData(DataEntity data) {
        List<DataEntity> overlappingData =
            serviceDAO.findOverlapping(data.getValidFrom(), data.getValidTo());
        if (!overlappingData.isEmpty()) {
            throw new IllegalStateException("Cannot have time overlaps");
        }
        serviceDAO.save(data);
    }
}

@Stateless
public class ServiceDAO {
    @Inject
    private EntityManager entityManager;

    public List<DataEntity> findOverlapping(Instant from, Instant to) {
        // finds records that have their validity interval overlap with the supplied one
    }

    public void save(DataEntity data) {
        entityManager.persist(data);
    }
}

If I understand EJBs correctly, the above code has the following implicit behaviour:

The @Stateless annotation turns the BusinessService and the ServiceDAO into EJBs.
BusinessService and ServiceDAO have container-concurrency-management by default
BusinessService#addData and ServiceDAO#addData have transactio type "REQUIRED" by default, because they are public EJB methods
Because BusinessService#addData is the outer EJB, it will open a new transaction, and ServiceDAO#addData will just continue it.

The problem I am facing is that two concurrent calls to /rest-service-path/add open two separate transactions, which will also perform the validation checks concurrently. If both pass the check, but the two datasets together would violate the constraint, they will still get added to the database, because they did not see each other due to the separate transactions.
My current workaround, which I am not too confident with though, is to make the BusinessService a @Singleton. This would implicitly give addData LockType.WRITE, meaning no two invocations of addData could ever run concurrently. This has the following problems:

It does not help if we had two or more separate java processes running the service, as the locking only works per-EJB.
It prevents concurrent processing for data entities that would not interfere with each other, which is the majority of cases for us. This causes worse performance

Did I understood all that correctly so far? How can I safely and more elegantly enforce a constraint defined in business code, such as the above? I am okay with exceptions being thrown inside the DAO, as long as it preserves consistency.

A:

I don't think you'll get around a singleton instance, but you can minimize the impact by not setting the EJBs to singleton, but introducing another service that manages data already being processed.
Essentially, it would contain a collection of DataEntitys being checked.
Those can be checked concurrently if they don't overlap, so you can maximize the concurrency by checking overlap between new candidates and ones in the queue first.
If a DataEntity is to be added but there's already one being checked that overlaps, it has to wait until that check is finished (after all, it's possible that the previous entity cannot be added but the recent one can). To do that, you'd need a mechanism to trigger the queued check later.

