15
\$\begingroup\$

As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.

The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.

Notes about Data Protection Questions:

  • Some questions are mandatory and some are not.
  • All mandatory questions must be answered in order to validate the caller.
  • At least one non-mandatory question must be answered in order to validate the caller.
  • Additional non-mandatory questions can remain unanswered.
  • Each question may have multiple correct answers.

Below are before and after screen shots of the Data Protection part of the Add Call view:

Before Data Protection has been validated

After Data Protection has been validated

The current system was written five years ago and we did not attempt to use any design patterns or modern approaches (such as Domain-Driven Design) because we lacked the time and understanding.

We now have the opportunity to re-write this software and would like to follow a Domain-driven approach.

First draft of a DataProtectionQuestion entity class:

public class DataProtectionQuestion
{
    public string Question { get; set; }
    public IEnumerable<string> Answers { get; set; }        
    public bool IsRequired { get; set; }
    public string Answer { get; set; }
    public bool IsAnswered => !string.IsNullOrWhiteSpace(Answer);
    public bool AnswerIsCorrect => Answers?.Contains(Answer) ?? false;
}

Questions that arose from this design:

  • Is this an anaemic model?
  • Is the DataProtectionQuestion entity the 'right' place to validate the answer?
  • Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
  • Should the setters be private and should clients supply data through a constructor?
  • Should an Answer be an entity in its own right with a property for 'IsValid'?
  • How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'? (I realise the UI is not the concern of the Domain, but having the ability to choose these as answers is)

Second draft (in an attempt to answer some of the above):

public class DataProtectionAnswer
{    
    public DataProtectionAnswer(string answer, bool isValid)
    {
        Answer = answer;
        IsValid = isValid;
    }
    public string Answer { get; private set; }
    public bool IsValid { get; private set; }
}

public class DataProtectionQuestion
{
    public DataProtectionQuestion(string question, bool isRequired, IEnumerable<DataProtectionAnswer> answers)
    {
        // validate non-null parameters?
        Question = question;
        IsRequired = isRequired;
        Answers = answers;
    }
    public string Question { get; private set; }
    public bool IsRequired { get; private set; }
    public IEnumerable<DataProtectionAnswer> Answers { get; private set; }
    public DataProtectionAnswer SelectedAnswer { get; private set; }
    public bool IsAnswered => SelectedAnswer != null;
    public bool AnswerIsCorrect => SelectedAnswer?.IsValid ?? false;
    public void SetSelectedAnswer(DataProtectionAnswer answer)
    {
        // Should validate that answer is not null and contained in Answers?
        SelectedAnswer = answer;
    }
}

Some answers..leading to more questions?:

  • Q. Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?
  • A. I have added a 'SetSelectedAnswer' method but I still don't know if this 'feels' right?
  • Q. Should the setters be private and should clients supply data through a constructor?
  • A. I don't know but that's what I've done in draft 2.
  • Q. Should an Answer be an entity in its own right with a property for 'IsValid'?
  • A. As per previous question, this is what I've done but should I have?
  • Q. How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?
  • A. I can now do this by adding an 'Unanswered' and 'Incorrect Answer' DataProtectionAnswer to the DataProtectionQuestion, but this 'feels' wrong. Isn't this the responsibility of the Presenter?

As you can probably tell, I'm floundering and really struggling to get my head around how to model this scenario using a DDD approach. Perhaps DDD isn't right for this situation. I don't know and I feel very stupid right now!

Can anyone please offer some guidance / suggestions on a way forward / better approach?

\$\endgroup\$
19
\$\begingroup\$

To start with, you're not doing DDD here.

DDD (Domain-Driven Design / Development) is based around the idea that we start with the domain. We don't touch code yet—we develop the domain models on-paper (or whiteboard, whatever is preferred). Once that is done, we build the code as closely to the domain as possible. The point of DDD is that the code should mirror the domain design.

Before we get going, I highly, highly, highly recommend this book, by Scott Wlaschin, a prominent F# developer who brings DDD into a very easy-to-understand view (the examples are F#, but they apply to C# as well): Domain Modeling made Functional

DDD is about:

  1. Define the domain, the inputs, and the outputs. That is, as a user of the system, what does the domain need to do. Here it sounds like we have part of the domain defined:

    As part of an insurance claims system we have created, the claims managers can log incoming telephone calls relating to a claim.

    The claims manager must validate the caller by asking a number of 'Data Protection' questions that are generated dynamically from information stored against the claim in a database. I believe this type of security is known as 'knowledge-based authentication'.

    Notes about Data Protection Questions:

    • Some questions are mandatory and some are not.
    • All mandatory questions must be answered in order to validate the caller.
    • At least one non-mandatory question must be answered in order to validate the caller.
    • Additional non-mandatory questions can remain unanswered.
    • Each question may have multiple correct answers.
  2. From there, we define our types. Generally, I do DDD with F#, but it's just as applicable to C#. We model the physical domain, so here we're not modeling the questions, we're modeling the validation. That is: the user must answer various questions and prove they are knowledgeable on the claim.

    This is the root of our domain model: we need to validate some information. You have mixed multiple pieces here, so we're going to separate them a bit.

  3. After building the types, we build the work. That is, the functions. We build the types as just data-structures, then we build the functions next to encapsulate the domain rules.

So, you've defined the domain (at least, as far as I see it) via the quoted-blurb, so what I want to do is move that into some types.

To start with, we'll define a DataProtectionResponse (we're going to use the exact language from the domain model, the purpose of DDD is to translate the human-language into code).

class DataProtectionResponse {
    public DataProtectionQuestion Question { get; set; }
    public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
    public Response Response { get; set; }
}

Now, we need to come up with a model for DataProtectionQuestion:

class DataProtectionQuestion {
    public string Question { get; set; }
    public bool Required { get; set; }
}

As you see, we are ONLY modeling two components of the question: the actual question, and whether or not it's required. The questions themselves are a different part of the domain, they're generated as a question, and using this is how we get into building a flexible model. We can now take these same questions somewhere else, and use them as a whole other tool, assuming it needs to interact with our current domain.

Next, we have ValidQuestionAnswer. This is going to be the answer that are valid for this particular claim:

class ValidQuestionAnswer {
    public Response Response { get; set; }
}

We made this a class as we absolutely want to consider a situation where an answer might have more data to it.

Finally, the Response. You might say, "Der Kommissar, why does that need to be a class, it's always a string?" Again, we might need to add more to this model, including functionality, so we do that by using a class.

class Response {
    public string Value { get; set; }
}

So now, our domain will consume an IEnumerable<DataProtectionResponse>, but not directly.

class DataProtection {
    public IEnumerable<DataProtectionResponse> Questions { get; set; }
}

Why another class? Well, let's start talking functionality.

First and foremost, the primary component of our design is that DataProtection must validate. For this to work, we need a IsValid function or property there:

public bool IsValid => Questions.All(x => x.IsSufficient);

Alright, so we have some concepts now. We have a IsValid that indicates if our DataProtection is valid or not, and we have decided that all of the questions must be sufficiently answered.

Next, we need to prove that a question is sufficiently answered.

public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));

Again, we are going to encode our actual logic: this DataProtectionResponse is sufficient if any of the ValidQuestionAnswers are acceptable with the question and response.

Next, how do we prove they're acceptable?

Well, the first rule is that if it's not required and there is no response, then it's valid:

if (!question.Required && response?.IsEmpty ?? false == false)
{
    return true;
}

And of course, Response.IsEmpty:

public bool IsEmpty => String.IsNullOrWhiteSpace(Value);

Otherwise, we want to prove that this response and the provided response are acceptable:

return response.Satisfies(Response);

And this is why we made it a class right-off-the-bat: we might have more logic that goes into Satisfies that might do heuristic analysis. I.e. if you provide an address, the logic might say 123 Main St. and 123 Main St and 123 Main Street are all the same.

public bool Acceptable(DataProtectionQuestion question, Response response)
{
    if (!question.Required && response?.IsEmpty ?? true)
    {
        return true;
    }

    return response.Satisfies(Response);
}

Next, our Response.Satisfies:

public bool Satisfies(Response response) => Value == response.Value;

And viola, we're done. We've encoded the entire domain, concisely, and using the actual terms the domain considers. Only 37 lines of code:

class Response
{
    public string Value { get; set; }
    public bool IsEmpty => String.IsNullOrWhiteSpace(Value);

    public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
    public Response Response { get; set; }

    public bool Acceptable(DataProtectionQuestion question, Response response)
    {
        if (!question.Required && response?.IsEmpty ?? true)
        {
            return true;
        }

        return response.Satisfies(Response);
    }
}
class DataProtectionQuestion
{
    public string Question { get; set; }
    public bool Required { get; set; }
}
class DataProtectionResponse
{
    public DataProtectionQuestion Question { get; set; }
    public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
    public Response Response { get; set; }
    public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
    public IEnumerable<DataProtectionResponse> Questions { get; set; }
    public bool IsValid => Questions.All(x => x.IsSufficient);
}

We don't have any odd logic, we don't have any conflated values: each model concerns itself with it's own work, no one else's.

Additionally, when our domain changes now (or we have to change the satisfaction logic) we have built the flexibility in-place without needing major infrastructure rewrites. If we need to override a response, we encode that in DataProtectionResponse and modify IsSufficient.

Finally, you could even shorten Acceptable to a single statement, since the logic is relatively straightforward:

class Response
{
    public string Value { get; set; }
    public bool IsEmpty => String.IsNullOrWhiteSpace(Value);

    public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
    public Response Response { get; set; }

    public bool Acceptable(DataProtectionQuestion question, Response response) =>
        (!question.Required && response?.IsEmpty ?? true) || response.Satisfies(Response);
}
class DataProtectionQuestion
{
    public string Question { get; set; }
    public bool Required { get; set; }
}
class DataProtectionResponse
{
    public DataProtectionQuestion Question { get; set; }
    public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
    public Response Response { get; set; }
    public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
    public IEnumerable<DataProtectionResponse> Questions { get; set; }
    public bool IsValid => Questions.All(x => x.IsSufficient);
}

You noted in the comments that I missed the requirement of at least one optional question, and you're absolutely right. So let's talk about another strength of DDD: verifying our code matches the domain.

At least one non-mandatory question must be answered in order to validate the caller.

So, looking through our code we see that we can start with DataProtection.IsValid:

public bool IsValid => Questions.All(x => x.IsSufficient);

Aha, we already know our problem is here. We make sure all questions are sufficient, but that does not take at least one optional into account. So how do we fix that?

Well, to start, we'll modify IsValid to support some theoretical work:

public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);

Here, we've decided that if each DataProtectionResponse tells us if it's required or not, and if it's answered or not, we can prove that we have at least one non-required question answered.

Next, we need to implement those two items. Both are trivial, and actually help with our other code:

public bool IsRequired => Question.Required;
public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));

Now we have another method to implement: ValidQuestionAnswer.AnsweredBy(Response):

public bool AnsweredBy(Response response) => response.Satisfies(Response);

You'll notice we've repeated one bit of code: response.Satisfies(Response) is in both AnsweredBy, and Acceptable, let's change that:

public bool Acceptable(DataProtectionQuestion question, Response response) =>
    (!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);

Once again, our contract is now satisfied, and we ONLY go at-most one-level-deep in each model from a parent model. (That is, we could have done !x.Question.Required instead of !x.IsRequired, but it's not the responsibility of DataProtection to know what makes a response required or not.)

So, 36 lines of code to build our new requirements:

class Response
{
    public string Value { get; set; }
    public bool IsEmpty => String.IsNullOrWhiteSpace(Value);

    public bool Satisfies(Response response) => Value == response.Value;
}
class ValidQuestionAnswer
{
    public Response Response { get; set; }

    public bool AnsweredBy(Response response) => response.Satisfies(Response);

    public bool Acceptable(DataProtectionQuestion question, Response response) =>
        (!question.Required && response?.IsEmpty ?? true) || AnsweredBy(Response);
}
class DataProtectionQuestion
{
    public string Question { get; set; }
    public bool Required { get; set; }
}
class DataProtectionResponse
{
    public DataProtectionQuestion Question { get; set; }
    public IEnumerable<ValidQuestionAnswer> ValidQuestionAnswers { get; set; }
    public Response Response { get; set; }

    public bool IsRequired => Question.Required;
    public bool IsAnswered => ValidQuestionAnswers.Any(x => x.AnsweredBy(Response));
    public bool IsSufficient => ValidQuestionAnswers.Any(x => x.Acceptable(Question, Response));
}
class DataProtection
{
    public IEnumerable<DataProtectionResponse> Questions { get; set; }
    public bool IsValid => Questions.All(x => x.IsSufficient) && Questions.Where(x => !x.IsRequired).Any(x => x.IsAnswered);
}

Finally, you asked a few questions about your implementation: as you see, I ignored them, purposefully. With DDD, those questions are not things to ask about the implementation but things to ask about the domain. With DDD we do iterations of "design", "implement", "design", "implement"—all the questions you have should go in the design stage, which is where you (and the other domain experts) gather and hash-out the principles of the project. This means, now that you have an implementation, we go back to design and clarify those questions. As the developer, when you see these things you should be creating a working list of potential problem-points, you might find out the domain-experts have considered them, or they may not, so you take your concerns back to them and refine the design. (Again, DDD is an iterative concept.)

But, suppose I were to answer them:

Should the entity have methods like 'SetAnswer' or 'SetAnswerIsValid'?

A: That's actually a design question, do you need to override whether an answer is valid or not? (I briefly touched on that in the first part of the answer.)

Should the setters be private and should clients supply data through a constructor?

A: This seems like an implementation question at first glance, but if we reword it things are different: can a client change an answer? If the answer is yes, the proposed design is fine. If not, model for immutability.

Should an Answer be an entity in its own right with a property for 'IsValid'?

A: In my humble opinion, yes. Answers aren't a string, they're a concept. Additionally, with a base-class, you can override that for answers that are bool, DateTime, etc. But, again: take it back to the DDD drawing board. The domain model will tell you what needs done.

How do I display answers in the UI to include 'Unanswered' and 'Incorrect Answer'?

A: That's a design question, but my suggestion is to ditch the Valid checkbox and provide a red/green "Needs Answered", "Incorrect Answer", or "Correct Answer" state. Again, do what the domain calls for. You're mixing some concerns here, and with DDD we create a clear separation. When you have a question like this, we go back into the design stage and hash it out. (It might turn out that you must not indicate if an answer is incorrect. Compliance laws are weird.)

\$\endgroup\$
  • \$\begingroup\$ Thank you for taking the time to write such a comprehensive response. It is much appreciated. You're absolutely right when you say I'm mixing concerns. I am struggling to get my mindset away from data-driven towards domain-drive design. Your suggested approach to DDD has given me much needed food for thought. One thing I mentioned in my original post that I don't think your answer covers: "At least one non-mandatory question must be answered in order to validate the caller." I believe the code you have suggested allows for all non-mandatory questions to be left unanswered? \$\endgroup\$ – datahandler Apr 3 at 8:27
  • \$\begingroup\$ @datahandler I just edited the answer to address that (I did not change the existing answer, but instead showed how we would validate that the model we built conforms to the DDD specification). Hopefully all of this helps you figure out where you can modify the process to do DDD a little more effectively. :) \$\endgroup\$ – Der Kommissar Apr 3 at 13:24

Not the answer you're looking for? Browse other questions tagged or ask your own question.