Building Maintainable PHP Applications: Accidental vs Essential Complexity
This article is part of the Building Maintainable PHP Applications series
Whenever a new feature is being implemented in a product, the developer’s first instinct is to implement it as simple as possible, meaning avoiding abstractions at all cost. The code is usually written in the controller because the feature does not seem like it’s very complex.
An example of such feature would be reservation in a office reservation system. It may start something like this, without having the whole feature planned out:
1<?php 2 3class UserReservationController extends Controller 4{ 5 public function create() 6 { 7 $data = validator(request()->all(), [ 8 'office_id' => ['required', 'integer'], 9 'start_date' => ['required', 'date:Y-m-d', 'after:today'],10 'end_date' => ['required', 'date:Y-m-d', 'after:start_date'],11 ])->validate();12 13 $office = Office::findOrFail($data['office_id']);14 15 $reservation = Reservation::create([16 'user_id' => auth()->id(),17 'office_id' => $office->id,18 'start_date' => $data['start_date'],19 'end_date' => $data['end_date'],20 'status' => Reservation::STATUS_ACTIVE,21 'price' => $numberOfDays * $office->price_per_day,22 'wifi_password' => Str::random()23 ]);24 25 return ReservationResource::make(26 $reservation->load('office')27 );28 }
As business rules and processes grow over time in a product, the product becomes more complex, which means the code will become more complex to manage as features are being implemented over time.
As new features come in, the developers get used to writing code in the controller since they probably don’t have any coding standards and rules what code goes where, and they might be thinking:
Here’s an example of how that code may end up being after a while:
1<?php 2 3class UserReservationController extends Controller 4{ 5 public function create() 6 { 7 abort_unless(auth()->user()->tokenCan('reservations.make'), 8 Response::HTTP_FORBIDDEN 9 );10 11 $data = validator(request()->all(), [12 'office_id' => ['required', 'integer'],13 'start_date' => ['required', 'date:Y-m-d', 'after:today'],14 'end_date' => ['required', 'date:Y-m-d', 'after:start_date'],15 ])->validate();16 17 try {18 $office = Office::findOrFail($data['office_id']);19 } catch (ModelNotFoundException $e) {20 throw ValidationException::withMessages([21 'office_id' => 'Invalid office_id'22 ]);23 }24 25 if ($office->user_id == auth()->id()) {26 throw ValidationException::withMessages([27 'office_id' => 'You cannot make a reservation on your own office'28 ]);29 }30 31 if ($office->hidden || $office->approval_status == Office::APPROVAL_PENDING) {32 throw ValidationException::withMessages([33 'office_id' => 'You cannot make a reservation on a hidden office'34 ]);35 }36 37 $reservation = Cache::lock('reservations_office_'.$office->id, 10)->block(3, function () use ($data, $office) {38 $numberOfDays = Carbon::parse($data['end_date'])->endOfDay()->diffInDays(39 Carbon::parse($data['start_date'])->startOfDay()40 ) + 1;41 42 if ($office->reservations()->activeBetween($data['start_date'], $data['end_date'])->exists()) {43 throw ValidationException::withMessages([44 'office_id' => 'You cannot make a reservation during this time'45 ]);46 }47 48 $price = $numberOfDays * $office->price_per_day;49 50 if ($numberOfDays >= 28 && $office->monthly_discount) {51 $price = $price - ($price * $office->monthly_discount / 100);52 }53 54 return Reservation::create([55 'user_id' => auth()->id(),56 'office_id' => $office->id,57 'start_date' => $data['start_date'],58 'end_date' => $data['end_date'],59 'status' => Reservation::STATUS_ACTIVE,60 'price' => $price,61 'wifi_password' => Str::random()62 ]);63 });64 65 Notification::send(auth()->user(), new NewUserReservation($reservation));66 Notification::send($office->user, new NewHostReservation($reservation));67 68 return ReservationResource::make(69 $reservation->load('office')70 );71 }72}
Now it’s not just a new reservation created in the database but rather:
Request validation
Specific reservation related business rules
Applying coupons. and price calculation business rules
Sending emails
Atomic locks to stop race conditions
other things that may need to be implement in the near future
The developers may not be seeing it but over time, they are creating what’s called accidental complexity, by not being careful or by not refactoring the code on time.
The code becomes harder to maintain and work with, very bug prone, which may or may not demotivate them to work with it.
Accidental and essential complexity are true not just for code, but also team processes, tools, frameworks and infrastructure.
In order to avoid the accidental complexity that is very common when building long term business software, additional thinking, planning and writing extra code is essential to deal with the complexity, making the code simple and easier to maintain.
The idea is to have control over the code and avoid changing it all over the place in the application, which reduces the time to change or implement something new and test it. The less moving pieces, the better.
Using different design patterns and principles can be very useful here, along with coding standards, structure and rules so that everyone can follow.
It requires discipline to think and work like that which may or may not slow down the team depending on their skill level and business knowledge.
The bigger the product complexity is, the harder it is to work on such products and that’s normal.
This is called essential complexity and we can’t really avoid it, that’s the nature of the businesses.
Developers do try to make their lives easier by making their lives harder when they are creating accidental complexity, which will not help the product for the long run except for the speed of implementation in the short term.
Choosing a specific architecture, patterns and principles depends on the context your application is in to tame the system’s essential complexity, it shouldn’t be done because someone told you to do that without understanding your context.
I’ll get back to the code example and refactor it in a future article.