Simple Leasing, Part 2: The Core
In Part 1, we set up a basic TypeScript project structure to fit the COILS architecture. Next, we’ll start filling out that project structure. The best starting point depends on a project’s purpose:
- Web or mobile apps often start with the UX because the UX is the app’s value proposition and needs to be the focus of the design.
- Data pipelines usually have a source and a sink, and determining how to load and process the former is a natural starting point.
- In “line of business” apps like Simple Leasing, we’re focused on the nouns and verbs of the system language that we’re building and can step directly into the business logic.
Entities and Relationships
When an application is a true greenfield project, I like to begin by thinking through the administrative tasks that are necessary for the system to work, even for tests. In this case, those are:
- An administrator configures the system with the leasable assets.
- An administrator configures users for managers.
The question of “users” is complex and often treated as an externality, so let’s defer that for now and focus on the first: leasable assets. Obviously, that means apartments, but there is typically some organization implied: apartments are located in a building, and buildings belong to a complex. We’ll focus our project on a single apartment complex, its buildings and their apartments, for simplicity.
Presented as a formal entity-relationship diagram,1 this constrained set of physical assets might look like this:
erDiagram COMPLEX ||--|{ BUILDING : contains BUILDING ||--|{ APARTMENT : contains
Diagrams are a powerful design tool, making it easy to express relationships between entities and evolving naturally with the design. For example, you might notice that all of these entities are separate physical places, and that it might be necessary to present them on a map or provide directions (not to mention having a place to look up the mailing address for the lessees later). Let’s add an address entity and make our ERD a little less linear:
erDiagram COMPLEX ||--|{ BUILDING : contains BUILDING ||--|{ APARTMENT : contains COMPLEX ||--|| ADDRESS : has BUILDING ||--|| ADDRESS : has APARTMENT ||--|| ADDRESS : has
Getting Down to Business (Logic)
At this point, I prefer to write code for these nouns and a few verbs that operate on them. We can build a full vertical slice of the system and sanity check our project structure. Some people prefer to defer writing code until later in the process, but this early stage is where I think powerful type systems really shine. Where should that code go, though?
In the previous post, we created a few directories:
src/
cli/
common/
core/
state/
web/
All of our business entities are part of the pure “Logic” part of the COILS
model, and should live in core
. These will be the primary nouns of the
language and must exist independently from external concerns. I’m going to be
very basic and call the module entities.ts
. All names are negotiable and can
be changed later, so there’s no need to agonize over them.
export type Address = {
address1: string;
address2: string | undefined; // Optional for unit numbers, etc.
city: string;
state: string;
zip: string;
};
export type Complex = {
name: string;
address: Address;
};
export type Building = {
name: string;
address: Address;
};
export type Apartment = {
number: string;
bedrooms: number;
bathrooms: number;
area: number;
address: Address;
};
Next, we turn to the verbs that operate on these nouns exclusively. In this
case, that means “…configures the system with leasable assets.” Since this is
an administrative task, it involves minimal business logic, and will be a
concern of input to a coordinator and data storage. Coordinators are the public
API of the core, so we can place them in core/index.ts
2.
The first thing to realize is that our entities don’t encode their own
relationships, so input to this sentence—which we’ll formalize as
createPhysicalAssets
—will need to do so.
// TypeScript doesn't allow named arguments, so an "args" type is a common
// pattern. "Props" is also a common name in UX contexts.
type CreatePhysicalAssetsArgs = {
// We're focusing on a single "Complex."
complex: Complex;
// The complex can contain multiple buildings...
buildings: {
building: Building,
// ..each of which contains multiple apartments.
apartments: Apartment[],
}[];
}
Coordinators are also responsible for instructing the State layer to retrieve
data we need or save data that we provide. We’ll need to provide the coordinator
with an interface that can represent the actions that it needs to specify to the
state layer. We’ll call that interface State
, and we can leave it as a stub
for now.
type State = {};
We can then implement the logic for this first function in terms of our entities, arguments, and state:
function createPhysicalAssets(state: State, args: CreatePhysicalAssetsArgs) {
// Record the complex first, as the root of our hierarchy.
const complex = state.saveComplex(args.complex);
// Iterate over the buildings, saving each...
for (const buildingArgs of args.buildings) {
const building = state.saveBuilding(complex, buildingArgs.building);
// ...and each apartment belonging to each building.
for (const apartment of buildingArgs.apartments) {
state.saveApartment(building, apartment);
}
}
}
This implementation helps add detail to the State
interface. You can do this
as you write the rest of the function, resolving LSP warnings with each, or
defer the interface definition until after you’re done:
type State = {
saveComplex(complex: Complex): Complex;
saveBuilding(complex: Complex, building: Building): Building;
saveApartment(building: Building, apartment: Apartment): Apartment;
};
There is nuance in the design. We don’t currently provide a way to update an
existing building without specifying its Complex
, for example. That’s not a
big issue yet, though, and we can always evolve this interface as we continue to
implement sentences from the business domain.
What’s Next?
One key aspect that is a big issue is that this pure State
is missing a key
factor: what if the state operation fails? Handling fallibility, or
impurity, differentiates the coordinator and state layers from the logic, and
right now we haven’t created a way to encode that impurity in the type system.
In the next article, we’ll introduce Result
types and continue to implement
createPhysicalAssets
as a “vertical slice” of the system.
Until then, try implementing the logic above yourself, and consider working ahead by following this implementation pattern for some of the other domain sentences.
Good luck!
-
It’s worth learning at least one style of ERDs and using a tool to generate them. Mermaid’s ERDs are good enough for most use cases and have the advantage of working in GitHub markdown, as of 2022. ↩︎
-
Nothing requires that all of your public API gets crammed into
index.ts
, of course, but it’s a useful convention to re-export public functions and types there. ↩︎