7  comments

We have the big picture of Domain-Driven Design already in our minds so now it's time to get coding. You might think that since we are building a Firebase app, we will need to worry about using the Firestore and FirebaseAuth classes right from the start. That's not true at all with DDD. Let's start in the most important layer of them all - the domain layer. Namely, we are going to tackle authentication.

Programmer & Project Safety Announcement

You are about to witness high amounts of abstraction throughout this tutorial series. Like with anything, pick and choose what you need and dismiss the rest.

For example, you may want to separate your app into layers and use Failures in place of Exceptions but instead of utilizing validated ValueObjects (we'll get to them in this part), you might want to validate data the usual way - only in the presentation layer as it's being inputted by the user and then pass around Strings and ints as you're used to.

This tutorial series will follow Domain-Driven Design religiously to show you what's possible. It's all up to you to choose what you deem to be an overkill. No matter how you decide to build apps in the end, knowing Domain-Driven Design will make you a better programmer who can look at problems from a new angle.

This post is a part of a series. See all the parts 👉 here 👈

Email & password

How can we sign in using email and password?  The usual way would be to have a sign in form that would validate the inputted Strings. You know, email addresses must have the '@' sign and passwords must be at least six characters long. We would then pass these Strings to the authentication service, in our case, Firebase Auth.

Sure, this is perfectly doable but we have to realize one important fact! Let's imagine we have a function which accepts two parameters.

unsuspecting_function.dart

Future<void> signIn({
  @required String email,
  @required String password,
}) async {
  // Sign in the user
}

Is it reasonable to call this function with the following arguments?

function_call.dart

signIn(email: 'pazzwrd', password: '[email protected]');

Of course, it isn't. But what stops us from passing an email address to a parameter expecting a password? They're all Strings, after all.


Type-safety evolution

The first thing we can do is to create simple classes for EmailAddress and Password. Let's focus on the former, so that we don't have to deal with two classes for now. By the way, we are going to be mostly inside the domain/auth folder. Check out the GitHub repository whenever you're unsure.

auth/email_address.dart

import 'package:meta/meta.dart';

@immutable
class EmailAddress {
  final String value;

  const EmailAddress(this.value) : assert(value != null);

  @override
  String toString() => 'EmailAddress($value)';

  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;
    return o is EmailAddress && o.value == value;
  }

  @override
  int get hashCode => value.hashCode;
}

This is much more expressive than a plain String plus we get an immediate non-null check. We also override the equality operator to perform value equality and also the toString() method to have a reasonable output.

A class like this is surely not ideal though. Yes, as soon as we have an EmailAddress instance, we cannot mistakenly pass it to a function expecting a Password. They're two different types. What we can do now though is the following.

instantiation.dart

void f() {
  const email = EmailAddress('pazzwrd');
  // Happily use the email address 
}

As you can see, we've escaped one problem only to get another one. Instances of EmailAddress happily accept any String into its constructor and then pretend like nothing happened if it doesn't fulfill the "contract" of what the EmailAddress represents. That's why we're going to create validated value objects.

Validating at instantiation

You are probably used to validating Strings in a TextFormField. (If not and you're still here, this series is not for you. Please, come back after you learn the basics.) Unless the TextFormField holds a valid value, you're not going to be able to save the Form and proceed with the invalid value.

We will take this principle and take it to a whole another level. You see, not all validation is equal. We're about to perform the safest validation of them all - we're going to make illegal states unrepresentable. In other words, we will make it impossible for a class like EmailAddress to hold an invalid value not just while it's in the TextFormField but throughout its whole lifespan.

Although we're going to get to everything in this series, I'd still recommend you to check out articles by Scott Wlaschin, where he goes through this topic in the F# language. These have been of huge help to me.

The most straightforward way of validating at instantiation is to create a factory constructor which will perform the validation logic by throwing Exceptions if something doesn't play right and then finally instantiate an EmailAddress by calling a private constructor.

auth/email_address.dart

@immutable
class EmailAddress {
  final String value;

  factory EmailAddress(String input) {
    assert(input != null);
    return EmailAddress._(
      validateEmailAddress(input),
    );
  }

  const EmailAddress._(this.value);

  // toString, equals, hashCode...
}

String validateEmailAddress(String input) {
  // Maybe not the most robust way of email validation but it's good enough
  const emailRegex =
      r"""^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~][email protected][a-zA-Z0-9]+\.[a-zA-Z]+""";
  if (RegExp(emailRegex).hasMatch(input)) {
    return input;
  } else {
    throw InvalidEmailException(failedValue: input);
  }
}

class InvalidEmailException implements Exception {
  final String failedValue;

  InvalidEmailException({@required this.failedValue});
}

We're definitely getting somewhere. Passing an invalid email string to the EmailAddress public factory will result in an InvalidEmailException being thrown. So yes, we do make illegal states unrepresentable.

To be honest though, if throwing exceptions were the only way we could prevent invalid values from being held inside validated value objects, you wouldn't be even reading this post because this series would never have happened. Why? Let's see what we have to do to instantiate just one EmailAddress

instantiation.dart

void f() {
  try {
  final email = EmailAddress('pazzwrd');
  } on InvalidEmailException catch (e) {
    // Do some exception handling here
  }
  // If you have multiple validators, remember to catch their exceptions too
}

Yeah, this is not the way to go. Creating this monstrosity everywhere you instantiate a validated value object would quickly become a painful and unmaintainable experience.

Either a failure or a value

Our current troubles stem from the fact that the EmailAddress class holds only a single field of type String. What if, instead of throwing an InvalidEmailException, we would instead somehow store it inside the class? And because we don't want to use Exceptions in an unconventional way, we'd create a plain old InvalidEmailFailure class.

This will allow us to not litter our codebase with try and catch statements at the time of instantiation. We will still need to handle the invalid value at the time of using the EmailAddress. We have to handle it somewhere, right?

However, we don't want to create a second class field called, for example, failure. I mean, would you remember to write the following everywhere you used an EmailAddress? And more importantly, would you even bother writing this code if it wasn't enforced on you?

usage_of_email_address.dart

void insideTheUI() {
  EmailAddress emailAddress;
  // ...
  if (emailAddress.failure == null) {
    // Display the valid email address
  } else {
    // Show an error Snackbar
  }
}

The code above is frankly horrible. It relies on nulls to represent missing values - this is a recipe for a disaster. What if we joined the value and failure fields into one by using a union type? And not just any sort of a union - we're going to use Either.

Either is a union type from the dartz package specifically suited to handle what we call "failures". It is a union of two values, commonly called Left and Right. The left side holds Failures and the right side holds the correct values, for example, Strings.

Additionally, we'll want to introduce a union type even for Failures. Although we currently have only one "ValueFailure" representing an invalid email address, we are going to have a bunch more of them throughout this series. Even here, unions will help us not to forget about any possible "case" of a ValueFailure.

So, we're going to use dartz for Either but what about the regular unions? There are multiple options to choose from until Dart introduces algebraic data types into the language itself. The best option is to use the freezed package. Let's add them to pubspec.yaml and since freezed uses code generation, we'll also add a bunch of other dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  dartz: ^0.9.0-dev.6
  freezed_annotation: ^0.7.1

dev_dependencies:
  build_runner:
  freezed: ^0.9.2

ValueFailure union

Before jumping back into the EmailAddress class, let's first ditch the InvalidEmailException in favor of the aforementioned union. We'll group all failures from validated value objects into one such union - ValueFailure. Since this is something common across features, we'll create the failures.dart file inside the domain/core folder. While we're at it, let's also create a "short password" failure.

To learn about all the other things freezed can do, check out its official documentation or my tutorial on an older version of it.

core/failures.dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'failures.freezed.dart';

@freezed
abstract class ValueFailure<T> with _$ValueFailure<T> {
  const factory ValueFailure.invalidEmail({
    @required T failedValue,
  }) = InvalidEmail<T>;
  const factory ValueFailure.shortPassword({
    @required T failedValue,
  }) = ShortPassword<T>;
}
We made the class generic because we will also validate values other than 
Strings later on in this series.

The value which can be held inside an EmailAddress will no longer be just a String. Instead, it will be Either<ValueFailure<String>, String>. The same will also be the return type of the validateEmailAddress function. Then, instead of throwing an exception, we're going to return the left side of Either.

auth/email_address.dart

@immutable
class EmailAddress {
  final Either<ValueFailure<String>, String> value;

  factory EmailAddress(String input) {
    assert(input != null);
    return EmailAddress._(
      validateEmailAddress(input),
    );
  }

  const EmailAddress._(this.value);

  // toString, equals, hashCode...
}

Either<ValueFailure<String>, String> validateEmailAddress(String input) {
  const emailRegex =
      r"""^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~][email protected][a-zA-Z0-9]+\.[a-zA-Z]+""";
  if (RegExp(emailRegex).hasMatch(input)) {
    return right(input);
  } else {
    return left(ValueFailure.invalidEmail(failedValue: input));
  }
}

Displaying the value held inside an EmailAddress object now doesn't leave any room for doubts. We simply have to handle the possible ValueFailure whether we feel like it or not.

some_widget.dart

void showingTheEmailAddressOrFailure(EmailAddress emailAddress) {
  // Longer to write but we can get the failure instance
  final emailText1 = emailAddress.value.fold(
    (left) => 'Failure happened, more precisely: $left',
    (right) => right,
  );

  // Shorter to write but we cannot get the failure instance
  final emailText2 =
      emailAddress.value.getOrElse(() => 'Some failure happened');
}

Password

EmailAddress is implemented and it contains a lot of boilerplate code for toString, ==, and hashCode overrides. We surely don't want to duplicate all of this into a Password class. This is a perfect opportunity to create a super class.

Abstract ValueObject

This abstract class will extend specific value objects across multiple features. We're going to create it under domain/core. All it does is just extracting boilerplate into one place. Of course, we heavily rely on generics to allow the value to be of any type.

core/value_objects.dart

@immutable
abstract class ValueObject<T> {
  const ValueObject();
  Either<ValueFailure<T>, T> get value;

  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;
    return o is ValueObject<T> && o.value == value;
  }

  @override
  int get hashCode => value.hashCode;

  @override
  String toString() => 'Value($value)';
}

We can now extend this class from EmailAddress. Not so bad now, huh?

auth/email_address.dart

class EmailAddress extends ValueObject<String> {
  @override
  final Either<ValueFailure<String>, String> value;

  factory EmailAddress(String input) {
    assert(input != null);
    return EmailAddress._(
      validateEmailAddress(input),
    );
  }

  const EmailAddress._(this.value);
}

Creating and organizing

Let's first bring order to our files before we go ahead to create yet another class and validation function. Feature-specific value objects will live inside their domain feature folders. In case of EmailAddress and Password, that's domain/auth.

As for the validation functions, I like to put all of them into a single file under domain/core.

The validation logic for a Password is extremely simple in our case. Just check the length of the input.

core/value_validators.dart

Either<ValueFailure<String>, String> validateEmailAddress(String input) {
  // Already implemented
}

Either<ValueFailure<String>, String> validatePassword(String input) {
  // You can also add some advanced password checks (uppercase/lowercase, at least 1 number, ...)
  if (input.length >= 6) {
    return right(input);
  } else {
    return left(ValueFailure.shortPassword(failedValue: input));
  }
}

The Password class will be almost identical to EmailAddress - except for the validation.

auth/value_objects.dart

class EmailAddress extends ValueObject<String> {
  // Already implemented
}

class Password extends ValueObject<String> {
  @override
  final Either<ValueFailure<String>, String> value;

  factory Password(String input) {
    assert(input != null);
    return Password._(
      validatePassword(input),
    );
  }

  const Password._(this.value);
}

Fundamentals are important

It took quite a long time to validate just two value objects, didn't it? Not quite because I actually took you through the whole process of coming up with the best solution of "making illegal states unrepresentable" in Dart. Once you have the ValueObject super class in place and you know what you're doing, creating something like a validated TodoName won't take more than a couple of minutes.

The best thing about having these specific value objects in places that would otherwise be just plain Strings is that you cannot possibly mess up, no matter how hard you try. We're using the Dart type system to guide us.

In the next part, we're going to write code in the application layer responsible for gluing together the UI with the authentication backend. Why didn't I say Firebase Auth? As you can imagine, we're going to use abstractions!

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪 and guitar 🎸

You may also like

Flutter Custom & Staggered Page Transition Animation Tutorial

Flutter Firebase & DDD Course [5] – Sign-In Form Logic

  • Hi Matt, I don’t like much putting all the feature-specific failures all inside a core-file since it will become a mess very quickly if the project is not too simple.
    How do you see to put them into specific files into the related feature directories?
    For example an “InvalidEmailFailure” and a “ShortPasswordFailure” classes in a “auth/failures.dart” file. Or maybe, to follow your structure, putting them into an “AuthFailure” as factory constructors as you did.

    Maybe the email one could remain in the core because an email is an email everywhere, but the password one looks strongly auth related to me. What if we have to handle two different kinds of passwords in our apps?

    I understand that your case is very simple and you won’t have these kinds of problems, I pointed out just to reason about it.

    • Hey Fabrizio!
      I totally understand your concerns – large apps need more organization.

      What I do is that I create feature-specific unions (AuthValueFailure) that contain feature-specific “case factories” (ShortPassword, InvalidEmail). This is all fine, but what if you also need to deal with some sort of a possible SettingsValueFailure being returned?

      One way is to implement a completely blank abstract class called ValueFailure by both AuthValueFailure and SettingsValueFailure. But that’s not great because you lose out on the code completion you get with unions 😢

      That’s why instead of implementing a common interface, I create a ValueFailure union which has Settings and Auth “case factories”. This way, you create a union of unions. It’s quite nice to work with.

  • Very nice explanation. This thus structure the code and brings in a better understanding of how to correctly validate objects. Thanks

  • Let’s say i want to test the instance of EmailAddress, is this test correct ?

    test(
    ‘EmailAddress cannot accept null argument’,
    () {
    // assert
    expect(EmailAddress(null), AssertionError);
    },
    );

    When i’m running this test i’m getting this error :-

    dart:core _AssertionError._throwNew
    package:flutterbackend/domain/auth/value_objects.dart 11:12 new EmailAddress
    test/domain/auth/value_objects_test.dart 20:14 main.

    ‘package:flutterbackend/domain/auth/value_objects.dart’: Failed assertion: line 11 pos 12: ‘input != null’: is not true.

  • Doesn’t freezed take care of things like operator, hashcode, and toString? Why did you have to copy all that into your ValueObject class?

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >