At the boundaries, applications aren't functional by Mark Seemann
Ten years later.
In 2011 I published an article titled At the Boundaries, Applications are Not Object-Oriented. It made the observation that at the edges of systems, where software interacts with either humans or other software, interactions aren't object-oriented. The data exchanged is exactly that: data.
A common catchphrase about object-oriented design is that objects are data with behaviour. In a user interface, a REST API, at the command line, etcetera, data is transmitted, but behaviour is not.
The article was one of those unfortunately unsatisfying articles that point out a problem without offering a solution. While I don't like to leave readers hanging like that, I on the other hand think that it's important sometimes to ask open-ended questions. Here's a problem. Can anyone offer a solution?
People occasionally come by that old article and ask me if I've changed my mind since then. The short answer is that I believe that the problem persists. I may, however, today have another approach to it.
Functional programming #
Starting around the time of the original article, I became interested in functional programming (FP). The more I've learned about it, the better it seems to address my concerns of eleven years ago.
I don't know if FP is a silver bullet (although I'd like to think so), but at least I find that I have fewer impedance mismatch issues than I did with object-oriented programming (OOP). In FP, after all, data is just data.
Except when it isn't, but more about that later.
It's easier to reconcile the FP notion of data with what actually goes on at the boundaries of a system.
Perhaps I should clarify what I mean by the word boundary. This is where a software system interacts with the rest of the world. This is where it receives HTTP requests and answers with HTTP responses. This is where it queries its database. This is where it sends emails. This is where it reads files from disk. This is where it repaints user interfaces. In short, the outermost circle in ports and adapters architecture.
Notice that what crosses the boundary tends to be data. JSON or XML documents, tabular data streams, flat files, etcetera. In OOP, your training tells you that you should associate some behaviour with data, and that's what creates dissonance. We introduce Data Transfer Objects (DTO) even though a DTO
"is one of those objects our mothers told us never to write."
We almost have to do violence to ourselves to introduce a DTO in an object-oriented code base, yet grudgingly we accept that this is probably better than the alternative.
In FP, on the other hand, that's just natural.
To paraphrase the simple example from the old article, imagine that you receive data like this as input:
{ "firstName": "Mark", "lastName": "Seemann" }
If we wanted to capture that structure as a type, in F# you'd write a type like this:
type Name = { FirstName : string; LastName : string }
And while you might feel icky defining such a DTO-like type in OOP, it's perfectly natural and expected in FP.
That type even supports JSON serialization and deserialization without further ceremony:
let opts = JsonSerializerOptions (PropertyNamingPolicy = JsonNamingPolicy.CamelCase) let n = JsonSerializer.Deserialize<Name> (json, opts)
where json
might contain the above JSON fragment.
To summarise so far: Data crosses the boundaries of systems - not objects. Since FP tends to separate data from behaviour, that paradigm feels more natural in that context.
Not all peaches and cream #
FP is, however, not a perfect fit either. Let's paraphrase the above medial summary: Data crosses the boundaries of systems - not functions.
FP is more than just functions and data. It's also about functions as first-class concepts, including functions as values, and higher-order functions. While an object is data with behaviour, closures are behaviour with data. Such first-class values cross system boundaries with the same ease as objects do: Not very well.
I was recently helping a client defining a domain-specific language (DSL). I wrote a proof of concept in Haskell based on Abstract Syntax Trees (AST). Each AST would typically be defined with copious use of lambda expressions. There was also an existential type involved.
At one time, the question of persistence came up. At first I thought that you could easily serialize those ASTs, since, after all, they're just immutable values. Then I realised that while that's true, most of the constituent values were functions. There's no clear way to serialize a function, other than in code.
Functions don't easily cross boundaries, for the same reasons that objects don't.
So while FP seems like a better fit when it comes to modelling data, at the boundaries, applications aren't functional.
Weak or strong typing? #
In my original article, I attempted to look ahead. One option I considered was dynamic typing. Instead of defining a DTO that mirrors a particular shape of data (e.g. a JSON document), would it be easier to parse or create a document on the fly, without a static type?
I occasionally find myself doing that, as you can see in my article To ID or not to ID. The code base already used Json.NET, and I found it easier to use the JToken
API to parse documents, rather than having to maintain a static type whose only raison d'être was to mirror a JSON document. I repeat an example here for your convenience:
// JToken -> UserData<string> let private parseUser (jobj : JToken) = let uid = jobj.["id"] |> string let fn = jobj.["firstName"] |> Option.ofObj |> Option.map string let ln = jobj.["lastName"] |> Option.ofObj |> Option.map string let email = jobj.["primaryEmail"] |> Option.ofObj |> Option.map (string >> MailAddress) { Id = uid FirstName = fn LastName = ln Email = email Identities = [] Permissions = [] }
Likewise, I also used JObject
to create and serialize new JSON documents. The To ID or not to ID article also contains an example of that.
F# types are typically one-liners, like that above Name
example. If that's true, why do I shy away from defining a static type that mirrors the desired JSON document structure?
Because typing (the other kind of typing) isn't a programming bottleneck. The cost of another type isn't the time it takes to write the code. It's the maintenance burden it adds.
One problem you'll commonly have when you define DTOs as static types is that the code base will contain more than one type that represents the same concept. If these types are too similar, it becomes difficult for programmers to distinguish between them. As a result, functionality may inadvertently be coupled to the wrong type.
Still, there can be good reasons to introduce a DTO type and a domain model. As Alexis King writes,
"Consider: what is a parser? Really, a parser is just a function that consumes less-structured input and produces more-structured output."
This is the approach I took in Code That Fits in Your Head. A ReservationDto
class has almost no invariants. It's just a bag of mutable and nullable properties; it's less-structured data.
The corresponding domain model Reservation
is more-structured. It's an immutable Value Object that guarantees various invariants: All required data is present, quantity is a natural number, you can't mutate the object, and it has structural equality.
In that case, ReservationDto
and Reservation
are sufficiently different. I consider their separate roles in the code base clear and distinct.
Conclusion #
In 2011 I observed that at the boundaries, applications aren't object-oriented. For years, I've thought it a natural corollary that likewise, at the boundaries, applications aren't functional, either. On the other hand, I don't think I've explicitly made that statement before.
A decade after 2011, I still find object-oriented programming incongruent with how software must work in reality. Functional programming offers an alternative that, while also not perfectly aligned with all data, seems better aligned with the needs of working software.