REST-first design is Imperative, DDD is Declarative [Comparison] - DDD w/ TypeScript

We cover this topic in The Software Essentialist online course. Check it out if you liked this post.
Also from the Domain-Driven Design with TypeScript series.
When you get a new Node.js project, what do you start coding first?
Do you start with the database schema?
Do you start with the RESTful API?
Do you start with the Models?
REST-first Design is a term I've been using it to describe the difference between what Domain-Driven Design projects and REST-first CRUD projects look like on a code level.
Just for reminders, REST stands for "Representational State Transfer", which is a architectural style towards designing APIs on the web with HTTP.
In this article, I'm going to explain what a REST-first Designed codebase looks like, how it's imperative and how it differs from a Domain-Driven Designed project.
Imperative vs. Declarative
Do we remember what imperative code is?
Imperative
You’ve probably written a lot of imperative code in your life, it’s usually the first thing we start out learning when we get into programming.
Imperative code is primarily concerned with "how" we do something. We need to be very explicit for how the program's state gets changed.
Find the max number in an array [Imperative]
Here's an example of how we would determine the max number in an array of numbers, imperative style.
const numbers = [1,2,3,4,5];
let max = -1;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > max) {
max = numbers[i]
}
}
Because imperative programming requires you to specify the exact commands to specify "how" program state changes, in this example, we:
- have a list of numbers
- create a for loop starting at 0
- increment i up to numbers.length
- and if number at the current index is greater than the max, then we’ll set that as the max
I'm sure you know how to code 😊 but I figured I'd list out every step to be succinct that this is what we do with imperative code. We define the "how".
REST-first Design often ends up being imperative by nature. We'll look closer at that statement in a few moments.
Declarative
Declarative programming is more concerned about the “what”.
Because of this, declarative code is a bit more "wordy" and abstracts away a lot of the details for expressability.
Let's look at the same example.
Find the max number in an array [Declarative]
const numbers = [1,2,3,4,5]
const max = numbers.reduce((a,b) => Math.max(a,b))
Take particular notice of the language being used in this example.
Ask yourself,
"which of the two examples would non-programmers be quicker to understand?"
Notice that in this example, the language better describes the “what” than the imperative equivalent?
That’s the beauty of declarative programming. Code is much more readable and program intent is easier understood (if operations are appropriately named).
Declarative style code is one of the primary benefits of designing software using Domain-Driven Design.
Now that we're refreshed on Imperative and Declarative style coding, we'll dive deeper into my statements.
REST-first Design
When we build RESTful applications, we tend to think more about designing our applications from either:
- the database up and
- the API calls up
Because of this, there's a tendency to place the majority of our business logic in either controllers or services.
You might remember from Uncle Bob's "Clean Architecture", for controllers this is definitely no-no.
And if you read his book of the same name, you might recall the potential service-oriented fallacy of putting all the domain logic into services (hint: Anemic Domain Models).
But this is the type of code that gets written when:
- we want to get something up and running quickly
- we use a framework, such as Nest.js, holistically
- we want to respond to prototype apps
- we're working on small apps
- we're working on problems that are either #1 or #2 from the Hard Software Problems
And it does suffice for a large number projects!
However, for complex domains with complicated business rules and policies, this has the potential to become incredibly difficult to change and extend as time goes on.
In REST-first CRUD applications, we almost solely write imperative code to satisfy business use cases. Let's take a look at what that looks like.
REST-first code
Let’s say we were working on an application where Customers
could rent Movies
.
Designing REST-first using Express.js and the Sequelize ORM, my code might look like this:
class MovieController {
public async rentMovie (movieId: string, customerId: string) {
// Sequelize ORM models
const { Movie, Customer, RentedMovie, CustomerCharge } = this.models;
// Get the raw orm records from Sequelize
const movie = await Movie.findOne({ where: { movie_id: movieId }});
const customer = await Customer.findOne({ where: { customer_id: customerId }});
// 401 error if not found
if (!!movie === false) {
return this.notFound('Movie not found')
}
// 401 error if not found
if (!!customer === false) {
return this.notFound('Customer not found')
}
// Create a record which signified a movie was rented
await RentedMovie.create({
customer_id: customerId,
movie_id: movieId
});
// Create a charge for this customer.
await CustomerCharge.create({
amount: movie.rentPrice
})
return this.ok();
}
}
In this code example, we pass in a movieId
and a customerId
, then pull out the appropriate Sequelize models that we know we’re going to need to use. We do a quick null check and then if both model instances are returned, we’ll create a RentedMovie
and a CustomerCharge
.
This is quick and dirty and it shows you just how quickly we can get things up and running REST-first.
But things start to get challenging as soon as we add business rules.
Business Rules in CRUD-first code
Let’s add some constraints to this. Consider that Customer
isn't allowed to rent a movie if they:
A) have rented the maximum amount of movies at one time (3, but this is configurable)
B) have unpaid balances.
How exactly can we enforce this business logic?
A primitive approach would be to enforce it directly in our MovieController
’s purchaseMovie
method like so.
class MovieController extends BaseController {
constructor (models) {
super();
this.models = models;
}
public async rentMovie () {
const { req } = this;
const { movieId } = req.params['movie'];
const { customerId } = req.params['customer'];
// We need to pull out one more model,
// CustomerPayment
const {
Movie,
Customer,
RentedMovie,
CustomerCharge,
CustomerPayment
} = this.models;
const movie = await Movie.findOne({ where: { movie_id: movieId }});
const customer = await Customer.findOne({ where: { customer_id: customerId }});
if (!!movie === false) {
return this.notFound('Movie not found')
}
if (!!customer === false) {
return this.notFound('Customer not found')
}
// Get the number of movies that this user has rented
const rentedMovies = await RentedMovie.findAll({ customer_id: customerId });
const numberRentedMovies = rentedMovies.length;
// Enforce the rule
if (numberRentedMovies >= 3) {
return this.fail('Customer already has the maxiumum number of rented movies');
}
// Get all the charges and payments so that we can
// determine if the user still owes money
const charges = await CustomerCharge.findAll({ customer_id: customerId });
const payments = await CustomerPayment.findAll({ customer_id: customerId });
const chargeDollars = charges.reduce((previousCharge, nextCharge) => {
return previousCharge.amount + nextCharge.amount;
});
const paymentDollars = payments.reduce((previousPayment, nextPayment) => {
return previousPayment.amount + nextPayment.amount;
})
// Enforce the second business rule
if (chargeDollars > paymentDollars) {
return this.fail('Customer has outstanding balance unpaid');
}
// If all else is good, we can continue
await RentedMovie.create({
customer_id: customerId,
movie_id: movieId
});
await CustomerCharge.create({
amount: movie.rentPrice
})
return this.ok();
}
}
There. It works. But there are several drawbacks.
Lack of encapsulation
Another developer could inadvertently circumvent our domain logic and business rules when developing a new feature that intersects with these rules, because it lives in a place where it shouldn’t be.
We could easily move this domain logic to a service. That would be a small improvement, but really, it’s just re-locating where the problem happens since other developers will still be able to write the code we just did, in a separate module, and circumvent the business rules.
_There are more reasons. If you'd like to know more about how services can get out of hand, read this.
There needs to be a single place to dictate what actions a Customer
can do, and that’s the domain model.
Lack of discoverability
When you look at a class and it’s methods for the first time, it should accurately describe to you the capabilities and limitations of that class. When we co-locate the capabilities and rules of the Customer
in an infrastructure concern (controllers), we lose some of that discoverability for what a Customer
can do and when it’s allowed to do it.
Lack of flexibility
Most of the time, we’re concerned about making our application deliverable through HTTP.
For CRUD applications, yes- this is usually how we will be delivering it since the world lives on RESTful APIs.
However, if you want your application to be multi-platform, integrate with an older system or deliver your application as a desktop app as well, we'll need to ensure that none of the business logic lives in controllers, and instead resides within the Domain Layer.
We'll want to do that so that different infrastructure technologies can execute the use cases of the application.
Going back to how this code is imperative, it's imperative because we have to specify exactly "how" everything happens.
When we purchase a movie, we're writing code to insert into a junction table.
That's not very declarative.
CRUD-first design is a “Transaction Script” approach
In the enterprise software world, Martin Fowler would call this a Transaction Script (article coming soon).
I first came to knowledge of the term after skimming through Fowler’s Patterns of Enterprise Application Architecture.
I also came to realize that the Transaction Script approach was the single approach I used to writing all of my backend code.
REST-first Design (more often than not) is a Transaction Script
How do we improve upon that? We use a domain model.
DDD
In Domain Modeling, one of the primary benefits is that we eventually hit an inflection point where the declarative language for specifying business rules becomes so expressive, that it takes us no time to add new capabilities and rules.
It also makes our business logic that much more readable, abstracting away how it gets done, and presenting more of what can get done and when it’s allowed to get done (that's not to say that the plumbing doesn't have to get laid).
If we were to take our previous example and look at it through DDD lenses, the controller code would probably look more like this:
class MovieController extends BaseController {
private movieRepo: IMovieRepo;
private customerRepo: ICustomerRepo;
constructor (movieRepo: IMovieRepo, customerRepo: ICustomerRepo) {
super();
this.movieRepo = movieRepo;
this.customerRepo = customerRepo;
}
public async rentMovie () {
const { req, movieRepo, customerRepo } = this;
const { movieId } = req.params['movie'];
const { customerId } = req.params['customer'];
const movie: Movie = await movieRepo.findById(movieId);
const customer: Customer = await customerRepo.findById(customerId);
if (!!movie === false) {
return this.fail('Movie not found')
}
if (!!customer === false) {
return this.fail('Customer not found')
}
// The declarative magic happens here.
const rentMovieResult: Result<Customer> = customer.rentMovie(movie);
if (rentMovieResult.isFailure) {
return this.fail(rentMovieResult.error)
} else {
// if we were using the Unit of Work pattern, we wouldn't
// need to manually save the customer at the end of the request.
await customerRepo.save(customer);
return this.ok();
}
}
}
See that? Notice how much is abstracted away?
From our controller, we no longer have to worry about:
- if the
Customer
has more than the max number of rented movies - if the
Customer
has paid their bills - billing the
Customer
after they rent the movie.
In following articles on Domain Entities and Aggregate Roots, we'll go into more depth on how this works.
This is the Declarative essence of DDD. How it is done is abstracted, but the ubiquitous language being used effectively describes what the domain objects are allowed to do and when.
Additional reading
3 Categories of Hard Software Problems
We cover this topic in The Software Essentialist online course. Check it out if you liked this post.
Discussion
Liked this? Sing it loud and proud 👨🎤.
Stay in touch!
Enjoying so far? Join 15000+ Software Essentialists getting my posts delivered straight to your inbox each week. I won't spam ya. 🖖
View more in Domain-Driven Design
You may also enjoy...
A few more related articles




Want to be notified when new content comes out?
Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.
6 Comments
Commenting has been disabled for now. To ask questions and discuss this post, join the community.
Great piece of content again! I've implemented a project in Nest.js that is going to grow in complexity a lot. I would love to take some learnings from the DDD approach and use them to refactor my backend. Can you be more specific on what exactly about Nest forces you into an anemic model, and what to focus on to get out of it?
Thanks!
Yeah, for sure :) great question btw. Love that.
OK, so an Anemic Domain Model is one where all of the domain logic ends up in the services and none of it ends up in the entities and value objects.
I can think of plenty of examples, but I'll stick to the topic of validation logic for now, and cover the rest of them in its own blog post.
Validation Logic
What are the required parameters to enforce object creation? Can we strictly-type validation rules?
To my knowledge, Nest.js requires you to define each of the attributes of an entity as a primitive.
So if I have a `User` class, the `email` field has to be `string`. This isn't great for encapsulating validation logic. I briefly answered your comment on this very problem in the value object article. We want to be able to have:
1) Good encapsulation of validation logic (so that means that instead of `userEmail: string`, we create a wrapper type to deal with `userEmail: UserEmail` instead, making illegal states unrepresentable.
2) Nominal typing of business objects (so even if we create a `UserEmail` to wrap a string, and we create another class called `UserPhoneNumber` which also wraps a string, we can only pass in `UserEmail` to `User.create(userEmail: UserEmail): User` and not `UserPhoneNumber` because the names of the types don't match, even though they wrap the same primitive.
3) Exposing only valid operations to the domain (if `User` has has a public setter for `userId`, does it make sense to the domain for me to be able to alter it from outside the class? For example, should you ever be able to change a `User` model's `userId` field? Probably not, right? Doing so would be invalid to the domain, and we'd corrupt that user. To my knowledge, Nest.js needs us to have all fields public so that it can create the columns at the persistence layer. Also, this tight coupling of the domain layer to the persistence layer breaks the dependency rule).
Thanks for detailed explanations! It looks great!
I have a question which I can't resist to ask.
I see most of the code in controller seems to be move to a service (Domain Layer) by just passing movieId and customerId right? This is because this code looks more to be operating on the domain data.
But, here the approach take was a bit different. It moved the checks to the customer class instead of a service and had all checks inside the controller.
Ideally both seem same. Can you please confirm?
Nice Article, Thank you.
I tend to look at the approach as UXDD which promotes a task-oriented design. in this design there is a user interface layer and there are workflows originated by that layer defined in a application layer which execute a business logic in domain layer.
"There needs to be a single place to dictate what actions a Customer can do, and that’s the domain model."
REST doesn't negate the involvement, existence and description of domain model changes. REST is not CRUD. you have not showed a true comparison of the terms
"A RESTful Microservices approach can improve the stability and resilience of services, reduces the need for extensive changes and redeployment when the domain model changes, and greatly increases the flexibility of individual services including the ability to automatically work with other newly-discovered services." (http://amundsen.com/talks/2018-02-sacon/index.html)
Where can i find next article: Domain Entities and Aggregate Roots which you mentioned in this article?
"(that's not to say that the plumbing doesn't have to get laid)"
And there's the rub. A lot of articles I've read on declarative/functional programming vs imperative programming (yours included) talk up declarative programming so much that they imply that imperative coding is a no-no. Yes, the controller code in your example is much more readable and elegant. But while the article holds up the improved controller code, it neglects to mention (besides the disclaimer I quoted) that all of that imperative code was simply moved to the customer and movie objects. To your credit, your article at least metaphorically states that imperative code still needs to be written, even as it implies that imperative coding is bad. I've read other articles that fail to mention imperative code still needs to be written at all.
If the goal of DDD is to encapsulate business logic into domain objects (much like an MVC implementation places them into the Model objects rather than the Controller objects), then MovieController contains (declarative) application logic rather than business logic. The imperative code that actually makes up the business logic still exists, just in a different object, where it belongs.
Without showing the implementations of the Customer and Movie domain objects, and the balance of declarative and imperative code within, calling DDD declarative in nature (ignoring its imperative aspects) as in your last paragraph can fail to come across well.