Implementing
failure causes
with dOOv

Welcome to the Furets!

@gdigugli – Gilles Di Guglielmo

  • Designer of sweet cooked software since 1999
  • Software Architect at LesFurets.com

  • 1 website, 5 Insurance Products : Car, Health, Home, Bike, Loan
  • 1 codebase, 450k lines of code, 60k unit tests, 150 selenium tests
  • 22 Developers, 2 DevOps, 4 Architects
  • 19 production servers including Load balancers, Frontend, Backend, Databases, BI
  • 1 release per day
  • 9 years of code history
  • 3M quotes/year, 40% of market share, 4M of customers

Introduction

LesFurets service orchestration

LesFurets service orchestration

Use case: goal

  • compliance: the rules correspond to the specification documents
  • auditability: understand a rule without looking at the code
  • governance: maintenance of the rules catalogue
  • clarity: productivity for developers

Live code

                
public static boolean validateAccount(User user,
                Account account, Configuration config) {
  if (config == null) {
      return false;
  }
  if (user == null || user.getBirthDate() == null) {
      return false;
  }
  if (account == null || account.getCountry() == null ||
      account.getPhoneNumber() == null) {
      return false;
  }
  if (YEARS.between(user.getBirthDate(), LocalDate.now()) >= 18
          && account.getEmail().length() <= config.getMaxEmailSize()
          && account.getCompany() == Company.LES_FURETS
          && account.getPhoneNumber().startsWith("+33")) {
      return true;
  }
  return false;
}
                
              
                
public class RulesOpenRndayTest {
  SampleModelRule demoRule = DslSampleModel
          .when(DOOV.matchAll(
                  userBirthdate.ageAt(today()).greaterOrEquals(18),
                  accountEmail.length().lesserOrEquals(configurationMaxEmailSize),
                  accountCompany.eq(Company.LES_FURETS),
                  accountPhoneNumber.startsWith("+33")))
          .validate();
}
                
              
                
@Test
public void test_account() {
    SampleModel sample = SampleModels.sample();
              
    Result result = demoRule.executeOn(sample);
              
    Assertions.assertThat(result).isTrue();
    System.out.println(demoRule.readable());
    System.out.println(demoRule.markdown(Locale.FRANCE));
}
                
              
                
rule when match all [user birthdate age at today >= 18, //
  account email length is <= configuration max email size, //
  account company = LES_FURETS, account phone number starts with '+33'] validate
* règle
  * lorsque
    * correspond à tous
      * la date de naissance âge à la date du jour >= 18
      * l'émail a une longueur <= la taille maximum de l'émail
      * la société = LES_FURETS
      * le numéro de téléphone commence par '+33'
  * valider
                
              
                
@Test
public void test_account_failure_cause() {
    SampleModel sample = SampleModels.sample();
    sample.getAccount().setPhoneNumber("+1 12 34 56 78");
              
    Result result = demoRule.executeOn(sample);
              
    Assertions.assertThat(result).isTrue();
}
                
              
                
java.lang.AssertionError: Expected result to be true 
  (invalidated nodes: [account phone number starts with '+33'])
                
              
                
@Test
public void test_account_failure_cause() {
    SampleModel sample = SampleModels.sample();
    sample.getAccount().setPhoneNumber("+1 12 34 56 78");
              
    Result result = demoRule.executeOn(sample);
              
    Assertions.assertThat(result)
            .isFalse()
            .hasFailureCause("account phone number starts with '+33'");
}
                
              
                
@Test
public void test_account_failure_cause_2() {
    SampleModel sample = SampleModels.sample();
    sample.getAccount().setPhoneNumber("+1 12 34 56 78");
    sample.getAccount().setCompany(Company.BLABLACAR);
              
    Result result = demoRule.withShortCircuit(false).executeOn(sample);
              
    Assertions.assertThat(result)
            .isFalse()
            .hasFailureCause("match all [account company = LES_FURETS, " +
                    "account phone number starts with '+33']");
}
                
              

Available predicate reductions

Failure cause - 1

Validate that a profile
has an email with less than 20 characters
has at least 18 years when their country is France
their country is France when their phone number starts with '+33'

                  
  DOOV.when(accountEmail.length().lesserThan(20)
        .and(userBirthdate.ageAt(today()).greaterThan(18)
          .and(accountCountry.eq(Country.FR)))
        .and(accountCountry.eq(Country.FR)
          .and(accountPhoneNumber.startsWith("+33"))))
      .validate();
                  
                

Failure cause - 1

                  
  model.getAccount().setEmail("test@test.org");
  model.getAccount().setCountry(Country.FR);
  model.getUser().setBirthDate(LocalDate.now().minusYears(19));
  
  ValidationRule rule = DOOV
      .when(accountEmail.length().lesserThan(20)  
        .and(userBirthdate.ageAt(today()).greaterThan(18)
            .and(accountCountry.eq(Country.FR)))
        .and(accountCountry.eq(Country.FR)
            .and(accountPhoneNumber.startsWith("+33"))))
      .validate();
  
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                  
                

                  
  > account phone number starts with '+33'
                  
                

Failure cause - 2

Validate that a profile
has at least 18 years when their country is France
and their phone number starts with '+33'
has at least 21 years when their country is Canadian
and their phone number starts with '+1'

                    
  DOOV.when(userBirthdate.ageAt(today()).greaterThan(18)
          .and(accountCountry.eq(Country.FR)
          .and(accountPhoneNumber.startsWith("+33")))
          .or(userBirthdate.ageAt(today()).greaterThan(21)
                .and(accountCountry.eq(Country.CAN)
                .and(accountPhoneNumber.startsWith("+1")))))
      .validate();
                    
                  

Failure cause - 2

                  
  model.getUser().setBirthDate(LocalDate.now().minusYears(22));
  model.getAccount().setCountry(Country.FR);
  
  ValidationRule rule = DOOV
      .when(userBirthdate.ageAt(today()).greaterThan(18)
          .and(accountCountry.eq(Country.FR)
          .and(accountPhoneNumber.startsWith("+33")))
          .or(userBirthdate.ageAt(today()).greaterThan(21)
                .and(accountCountry.eq(Country.CAN)
                .and(accountPhoneNumber.startsWith("+1")))))
      .validate();
  
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                  
                

                  
  > account phone number starts with '+33'
      or (account country = CAN and
          account phone number starts with '+1')
                  
                

Failure cause - 3

Validate that a profile
has at least 18 years when their country is France
and their phone number starts with '+33'
has at least 21 years when their country is Canadian
and their phone number starts with '+1'

                    
  DOOV.when(matchAny(
          matchAll(userBirthdate.ageAt(today()).greaterThan(18),
                   accountCountry.eq(Country.FR),
                   accountPhoneNumber.startsWith("+33")),
          matchAll(userBirthdate.ageAt(today()).greaterThan(21),
                   accountCountry.eq(Country.CAN),
                   accountPhoneNumber.startsWith("+1"))))
       .validate();
                   
                  

Failure cause - 3

                  
  model.getUser().setBirthDate(LocalDate.now().minusYears(22));
  model.getAccount().setCountry(Country.FR);
  
  ValidationRule rule = DOOV
      .when(matchAny(
          matchAll(userBirthdate.ageAt(today()).greaterThan(18),
                   accountCountry.eq(Country.FR),
                   accountPhoneNumber.startsWith("+33")),
          matchAll(userBirthdate.ageAt(today()).greaterThan(21),
                   accountCountry.eq(Country.CAN),
                   accountPhoneNumber.startsWith("+1"))))
      .validate();
  
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                  
                

                  
  > match any [account phone number starts with '+33',
               match all [account country = CAN,
                          account phone number starts with '+1']]
                  
                

Failure cause - 4

Validate that a profile country is Canadian or French

                  
  DOOV.when(accountCountry.anyMatch(Country.CAN, Country.FR)).validate();
                  
                

Failure cause - 4

                  
  model.getAccount().setCountry(Country.UK);
  
  ValidationRule rule = DOOV
      .when(accountCountry.anyMatch(Country.CAN, Country.FR))
      .validate();
  
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                  
                

                  
  > account country != UK
                  
                

Failure cause - 5

Validate that a profile match at least two conditions :

  • profile has at least 18 years
  • country is France
  • phone number starts with '+33'

                  
  DOOV.when(count(
        userBirthdate.ageAt(today()).greaterThan(18),
        accountCountry.eq(Country.FR),
        accountPhoneNumber.startsWith("+33")).greaterThan(1))
      .validate();
                  
                

Failure cause - 5

                  
  model.getUser().setBirthDate(LocalDate.now().minusYears(19));
  model.getAccount().setCountry(Country.CAN);
  model.getAccount().setPhoneNumber("1 23 45 67 89");
  
  ValidationRule rule = DOOV
      .when(count(
              userBirthdate.ageAt(today()).greaterThan(18),
              accountCountry.eq(Country.FR),
              accountPhoneNumber.startsWith("+33"))
          .greaterThan(1))
      .validate();
  
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                  
                

                  
  > account country = FR and account phone number starts with '+33'
                  
            

Going further

Validate that the company of an account should NOT be
Dailymotion or Blablacar

              
DOOV.when(accountCompany.noneMatch(DAILYMOTION, BLABLACAR)).validate();
              
            

Rewritting #1

                
model.getAccount().setCompany(DAILYMOTION);

ValidationRule rule = DOOV
      .when(accountCompany.noneMatch(DAILYMOTION, BLABLACAR))
      .validate();
Result result = rule.withShortCircuit(false).executeOn(wrapper);
System.out.println("> " + result.getFailureCause());
                  
                

                  
  > account company match none  : DAILYMOTION, BLABLACAR
                  
            

Rewritting #2

                  
DOOV.when(accountCompany.noneMatch(DAILYMOTION, BLABLACAR)).validate();
                  
                

could be rewritten

                
DOOV.when(accountCompany.notEq(DAILYMOTION)
            .and(accountCompany.notEq(BLABLACAR))).validate();
            
          

              
model.getAccount().setCompany(DAILYMOTION);
Result result = rule.withShortCircuit(false).executeOn(wrapper);
System.out.println("> " + result.getFailureCause());
              
            

                    
  > account company != DAILYMOTION
                    
              

Rewritting #3

                    
  DOOV.when(accountCompany.noneMatch(DAILYMOTION, BLABLACAR)).validate();
                    
                  

could be rewritten

                  
  DOOV.when(accountCompany.eq(DAILYMOTION)
          .or(accountCompany.eq(BLABLACAR)).not())
             .validate();
              
            

                
  model.getAccount().setCompany(DAILYMOTION);
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                
              

                      
    > not (account company = DAILYMOTION or account company = BLABLACAR)
                      
                

Rewritting #4

                      
DOOV.when(accountCompany.noneMatch(DAILYMOTION, BLABLACAR)).validate();
                      
                    

could be rewritten

                    
DOOV.when(accountCompany.anyMatch(LES_FURETS, CANAL_PLUS, MEETIC, OODRIVE))
        .validate();
                
              

                  
model.getAccount().setCompany(DAILYMOTION);
Result result = rule.withShortCircuit(false).executeOn(wrapper);
System.out.println("> " + result.getFailureCause());
                  
                

                
> account company != DAILYMOTION
                
              

Rewritting #5

                
  DOOV.when(accountCompany.noneMatch(DAILYMOTION, BLABLACAR)).validate();
                
              

could be rewritten

                      
  DOOV.when(
    accountCompany.eq(LES_FURETS).or(accountCompany.eq(CANAL_PLUS)
            .or(accountCompany.eq(MEETIC).or(accountCompany.eq(OODRIVE)))))
                    .validate();
                  
                

                    
  model.getAccount().setCompany(DAILYMOTION);
  Result result = rule.withShortCircuit(false).executeOn(wrapper);
  System.out.println("> " + result.getFailureCause());
                    
                  

                  
  > account company = LES_FURETS or (account company = CANAL_PLUS 
         or (account company = MEETIC or account company = OODRIVE))
                  
                

Speed is not enough

                  
Benchmark                                        Mode  Cnt     Score      Error   Units
test_account_1                                   thrpt   20  8775.818 ± 148.951  ops/ms
test_account_2  [failure cause OK]               thrpt   20  5022.391 ± 147.550  ops/ms
test_account_3                                   thrpt   20  3022.433 ± 586.881  ops/ms
test_account_4  [failure cause OK]               thrpt   20  6002.415 ±  94.531  ops/ms
test_account_5                                   thrpt   20  1855.837 ±  50.183  ops/ms
                  
                

                    
Benchmark                                        Mode  Cnt     Score      Error   Units
test_account_1_short_circuit                     thrpt   20  6839.429 ± 262.318  ops/ms
test_account_2_short_circuit                     thrpt   20  7397.586 ± 252.090  ops/ms
test_account_3_short_circuit                     thrpt   20  5084.227 ± 450.013  ops/ms
test_account_4_short_circuit                     thrpt   20  6275.185 ±  56.600  ops/ms
test_account_5_short_circuit                     thrpt   20  1820.198 ±  60.300  ops/ms
                    
                  

#2 approach is the overall winner
#4 approach is the winner only for listed values

Enjoy dOOv.org

http://www.dOOv.org