When you use the lazy load strategy, you are basically telling it to load on demand or when requested (link). By default, the relations that are lazy loaded are @OneToMany and @ManyToMany while for associations @OneToOne and @ManyToOne are used the strategy eager. It makes sense, if you have a huge collection is very inefficient to load all the elements of the collection. In the case of @OneToOne or @ManyToOne you just have one entity/object related.
About your question, it looks that Hibernate although you explicitly use the lazy load strategy, because of the behaviour of the annotation NaturalId in branchId and costumerCode may decide to load the entity costumer as eager.
We can designate a field as a natural identifier simply by annotating it with @NaturalId. This allows us to seamlessly query the associated column using Hibernate’s API.
You indicate that the otherCostumer field uses the lazy load strategy perfectly. In that case, it seems that the use of the NaturalId with the annotation JoinColumns is the cause of the problem.
Note:
I tested your code and this is the query generated in that case:
select i1_0.id,i1_0.branch_id,i1_0.customer_code from invoice i1_0
select c1_0.id,c1_0.branch_id,c1_0.customer_code from customer c1_0 where (c1_0.branch_id,c1_0.customer_code) in((?,?))
In the case you remove/comment the annotation JoinColumns you have the following query:
select i1_0.id,i1_0.customer_id from invoice i1_0
Basically, it's adding the where clause (where (c1_0.branch_id,c1_0.customer_code) in((?,?)))
In general, because you mentioned it for the n + 1 query problem I would use JPA Entity Graph. It allow us to define a network of components and attributes to load into a query.
In my opinion, I Would not use the lazy load strategy for the @OneToMany. If you don't want to return the field (customer) use the DTO pattern and only return the fields you need. Otherwise, you have the option mentioned above as well.