This is already hinted at on this question, but to be more direct, you could consider using Set<Id>
instead:
public static Set<Id> cache = new Set<Id>();
public static Boolean firstRun(Set<Id> recordIds) {
return cache.addAll(recordIds);
}
However, keep in mind that partial updates will also cause this problem because the state won't be rolled back. In this case, I'd also recommend undoing firstRun at the end of each trigger:
public static void finish(Set<Id> recordIds) {
cache.removeAll(recordids);
}
At that point, your triggers should be designed as follows:
if(Util.firstRun(Trigger.new)) {
// do trigger logic here
Util.finish(Trigger.new);
}
Note that if you're trying to stop the second update from a workflow field update, then you'll want to skip calling the finish method. However, you need to consider which type of recursion you're trying to stop.
Finally, when possible, consider using state-based changes instead of depending on blocking recursive updates. For example, let's say you want to update all contact phone numbers when the account phone number changes, you'd want to query only for contacts that don't already have the updated phone number.