Domain-Driven Design in JavaScript
Let DDD bring order to your JavaScript chaos.
Join the DZone community and get the full member experience.
Join For FreeI wouldn't class myself as a JavaScript developer, I always joke that it's a language I never meant to learn. It's so pervasive now, it just happened. I go through phases of enjoying it and despising it. But through the peaks and troughs of love and not quite hate. One problem persisted: if I'm to be a good JS developer and write functional JavaScript, how then do I write code in a way that implies a proper domain model?
In traditional OO languages, such as Java, C#, and even Go actually, it's easy to write code that's architected around a domain design. You have classes, which are big and do a lot of stuff. Which of course is something you generally avoid like the plague in JavaScript, for fair enough reasons.
However, my code always seemed to end up looking like this:
const { getUser, removeUser } = require('services/user');
const { sendEmail } = require('helpers/email');
const { pushNotification } = require('helpers/notifications');
const { removeFilesByUserId } = require('services/files');
const removeUserHandler = await (userId) => {
const message = 'Your account has been deleted';
try {
const user = await getUser(userId);
await removeUser(userId);
await sendEmail(userId, message);
await pushNotification(userId, message);
} catch (e) {
console.error(e);
sendLogs('removeUserHandler', e);
};
return true;
};
This looks okay, right? Sure! No big problems here design-wise. However, when you have a large codebase entirely made up of files such as this, in other words directories full of vaguely grouped 'services,' individually exporting and importing single functions, often vaguely named, and not obviously belonging to a domain when reading through the code, it can very quickly feel as though you're dealing with a big ball of unrelated scripts, rather than a well-architected software application.
I didn't want to return to classes and traditional encapsulation. It felt like a step back after learning 'the functional way™️. But, increasingly, I was finding JavaScript projects difficult to read, 'bitty' and fragmented. I was seeing this everywhere, too! It wasn't just my own hapless downfall. It seemed really common to see JS projects with little to no design or architecture. I was ready to toss JS into the bin for good and resume my position in the Golang ivory tower.
Until one of my engineers slipped a new feature into one of our most noisy codebases, which jolted my attention.
Peering through reams and reams of JavaScript, suddenly something stood out in a PR.
ScheduledJobs.run(jobId);
const job = await ScheduledJobs.get(jobId);
Huh. Is that, a class? Surely not. We don't do that here! No!
const run = (jobId) => {};
const stop = (jobId) => {};
const pause = (jobId) => {};
const get = (jobId) => {};
module.exports = {
run,
stop,
pause,
get,
};
Praise Dijkstra, they're just functions! Good old-fashioned functions. Suddenly I felt so, so very silly for deliberating, Googling manically for weeks and weeks, and posting lengthy diatribes on Twitter about how JavaScript was done; not fit for public consumption. When all I needed to do was use what JavaScript gave me for this exact purpose: modules! I got so caught up in trying to follow a paradigm that I forgot to be pragmatic.
If I refactored my first arbitrary example to use this pattern, in order to follow a domain design, maybe I'd have something more like this:
const UserModel = require('models/user');
const EmailService = require('services/email');
const NotificationService = require('services/notification');
const FileModel = require('models/file');
const Logger = require('services/logger');
const removeUserHandler = await (userId) => {
const message = 'Your account has been deleted';
try {
const user = await UserModel.getUser(userId);
await UserModel.removeUser(userId);
await EmailService.send(userId, message);
await NotificationService.push(userId, message);
return true;
} catch (e) {
console.error(e);
Logger.send('removeUserHandler', e);
};
return true;
};
This code tells me so much more already!
I began writing my JavaScript in this way, centered around these objects of grouped functions, which can still be used in a functional way. But this pattern communicates purpose much better than dealing in lots of single, un-grouped function calls. I find it made code easier to follow, having that indicator of where this piece of code fits into the bigger picture.
It was so simple in the end, and it was something I already knew, even something I had already used hundreds of times in the past. It all seemed so obvious! But it's easy to neglect concepts such as DDD in languages like JavaScript, especially when you're on the pursuit to functional enlightenment! But there is a happy medium.
Published at DZone with permission of Ewan Valentine, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments