Domain-Driven Design With Laravel - 6
Domain-Driven Design With Laravel - 6
1 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Introduction
In this short case study, we will design a portfolio and dividend tracker application. First of
all, let's define what the application does. It has four main features:
You can see the invested capital in each stock and portfolio.
The current market value of them.
And also yields such as dividend yield, or the overall yield (based on invested
capital and current market value).
A summary of received dividends.
How much money did you receive this week, this month, or all time?
A monthly view of received dividends.
There are no CRUD actions in this system, so you cannot create a new holding or anything
like that. All of the data is constructed by CSV imports. The market values are updated
using a 3rd party API called Market Stack.
2 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Data Modeling
It's a small application, but let's go through the database and models because it's
important.
3 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Stock
In the context of a portfolio tracker, a stock is a lightweight model. As you can see, it only
has three essential columns:
ticker
dividend_amount_per_year is the total $ amount of the dividend paid by this
company each year.
dividend_times_per_year is a number that represents how many times this
company distributes its dividend to the shareholders.
1 AAPL 0.88 4
2 MSFT 2.48 4
The first row means Apple pays a $0.88 dividend a year and it sends you a whopping $0.22
each quarter so 4 times a year. In the case of Microsoft, they will reward you with $2.48 a
year or $0.62 every quarter. These numbers apply to one share.
Why not call this table companies ? At the end of the day, it contains information about
publicly-traded companies. That's correct, however, I chose the name stock because
it's very lightweight. I've been working on other financial applications and usually, a
company means much more than this. It doesn't even have a name.
As you can see, I'm very specific with the column names. It's not just
dividend_amount , it's dividend_amount_per_year . It's because dividends are
measured per year but are usually distributed quarterly. So it would be very ambiguous
if it was called dividend_amount .
4 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Holding
When an investor purchases a stock, it becomes their holding. You can think about it this
way: there's only one Apple, but millions of Apple stocks are owned by individual investors.
Each person invests a different amount of money into AAPL stock. Let me show you an
example:
After these actions, the holdings table would look like this:
You invested $210 into Apple and bought two shares, so your average cost is $105. The
average cost is very important because this is how we can calculate the gains. In this
example, you gained 14.28%.
As you can see, this table is the basis of every critical calculation. How does this table gets
populated? By importing the transactions.
Transaction
Whenever you buy or sell a share, it's a transaction. And, of course, there's always a stock
involved. There are several important attributes:
5 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
In this application, I decided to store the total_price and the quantity as negative
numbers if the transaction is a sell. This is beneficial because it's extremely easy to sum
them up:
!" 1 + 4 + (-1) = 4
$holding!#quantity = $transactions!#sum('quantity');
!" 100 + 440 + (-120) = 420
$holding!#invested_capital = $transaction!#sum('total_price');
As I said earlier, transactions become holdings when importing a CSV. So the three rows in
the example will become one holding of AAPL. And with negative numbers, it's
straightforward to calculate the properties of the holding. This choice heavily depends on
the concrete use-cases. In this instance, it's a good idea because it makes things easier.
Dividend Payout
This table contains the individual dividend payments from your holdings. Here are some
examples:
1 1 0.2050 2021-02-11 1
2 1 0.2200 2021-05-10 1
3 1 0.2200 2021-08-12 1
4 1 0.2200 2021-11-11 1
6 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
These four rows contain the quarterly paid dividends of Apple in 2021. Each row belongs to
a holding. It's important to note that it belongs to a holding, not a stock. So this example
assumes that the holding's quantity is precisely 1. If you have 2 Apple shares instead, the
numbers will double up. So instead of 0.2050, it would be 0.4100, and instead of 0.2200,
it'd be 0.4400.
We can calculate the sum of dividends paid by a holding of yours from this table. So if your
average_cost for Apple is $100 and you got these dividends:
Portfolio
A portfolio is just a container for holdings. It's not an essential model from a technical point
of view, but it's important for users. A portfolio has two aggregate numbers:
Invested capital
Market value
Each of them equals the sum of the individual holdings' invested capital or market value.
7 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Custom Collections
Weighted Average Price
Another remarkable aspect of Laravel I haven't talked about in the book is custom
collections. In this application, I have transactions. At some point, transactions become
holdings. Because of this, I need to do three things frequently when I work with a collection
of transactions:
$transactions = Transaction!$all();
$sumOfProducts = $transactions
!#sum(fn (Transaction $transaction) !%
$transaction!#quantity * $transaction!#price_per_share
);
Now, where do you put this code? Let's go through some options:
8 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Now, my main problem is this: it has nothing to do with the transaction model. It's a static
method; it does not use the $this, so, in my opinion, it doesn't belong to the Transaction
model. My general rule: if you have a static method in a model, you probably can find a
better place for it.
If you think about this snippet, it only exists in the context of a collection, right? Fortunately,
in Laravel, we can write a custom collection for a model:
9 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
namespace App\Collections\Transaction;
use App\Models\Transaction;
use Illuminate\Database\Eloquent\Collection;
10 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
namespace App\Models;
use App\Collections\Transaction\TransactionCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
$transactions = Transaction!$all();
$holding!#price_per_share = $transactions!#weightedPricePerShare();
And here's the important thing. A transaction doesn't have a weighted price per share. But
a collection of transactions do have. The example above expresses this perfectly. So by
using custom collections, we can write more expressive, domain-oriented code.
Since we want to sum everything when it comes to transactions, let's add some more
methods to the collection class:
11 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
namespace App\Collections\Transaction;
use App\Models\Transaction;
use Illuminate\Database\Eloquent\Collection;
And finally, this is one part of the app that uses this collection:
12 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
class CreateHoldingsFromTransactions
{
public function execute(
TransactionCollection $transactions,
User $user
): Collection {
return $transactions
!#groupBy('stock_id')
!#map(fn (TransactionCollection $transactions, int $stockId) !%
Holding!$updateOrCreate(
[
'stock_id' !% $stockId,
'user_id' !% $user!#id,
],
[
'average_cost' !% $transactions!#weightedPricePerShare(),
'quantity' !% $transactions!#sumQuantity(),
'invested_capital' !% $transactions!#sumTotalPrice(),
'ticker' !% $transactions!#first()!#stock!#ticker,
]
)
);
}
}
It's very high-level, in my opinion. And it helps you to bring the code closer to the domain
language, which is very important for me. You can apply this technique to a significant
number of things.
13 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Yield On Cost
The yield on cost (YoC) is the dividend payout divided by the average cost. Now, we can
calculate YoC on a great number of things:
An individual holding.
A number of holdings.
A whole portfolio.
A number of portfolios together.
Let's see some examples. This is how the YoC is calculated on a holding:
For example, if Apple pays $0.865 every year and my average cost is $100, this will return
0.0865 or 0.865%. With a custom collection, we can also calculate the YoC on a number of
holdings:
14 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
namespace App\Collections\Holding;
use App\Models\Holding;
use Illuminate\Database\Eloquent\Collection;
This is the weighted average of YoCs. However, in real life, there's no such thing as
"weighted average of yield on costs"; it's just yield on cost. This is why I'm using the
function name yieldOnCost .
15 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
namespace App\Builders\Portfolio;
use App\Models\Holding;
use Illuminate\Database\Eloquent\Builder;
return $holdings!#yieldOnCost();
}
}
Since a portfolio is just a collection of holdings, we can use the HoldingCollection here.
The is_aggregate means that this portfolio is the "All" for the user, so it contains every
other portfolio.
16 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
DTOs
Collections
Value Objects
17 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Each external service has a main class called XYZService ; in this case, it's
MarketStackService . This is the class that sends HTTP requests and parses responses.
This class usually has several arguments in the constructor:
class MarketStackService
{
public function !'construct(
private readonly string $uri,
private readonly string $accessKey,
private readonly int $timeout
) {}
}
These config values come from the .env file, where I always prefix these variables with
the service's name:
MARKET_STACK_ACCESS_KEY=YOUR_TOKEN_HERE
MARKET_STACK_URI=http:!"api.marketstack.com/v1/
MARKET_STACK_TIMEOUT=5
Laravel includes a config/services.php file by default. This can be used to read the
external service's config from the env:
return [
'market_stack' !% [
'access_key' !% env('MARKET_STACK_ACCESS_KEY'),
'uri' !% env('MARKET_STACK_URI', 'http:!"api.marketstack.com/v1/'),
'timeout' !% env('MARKET_STACK_TIMEOUT', 5),
],
];
So in this file, each service has its array key. Notice that the array keys inside
market_stack are not prefixed.
18 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
As the last step, I create a separate service provider for every 3rd party, such as this one:
namespace App\Providers;
use App\Services\MarketStack\MarketStackService;
use Illuminate\Support\ServiceProvider;
I construct a new instance of the MarketStackService and bind it into the service
container in the boot method. Usually, this bind can be a singleton since we only need one
instance of MarketStackService . Since this class contains scalar values (such as the
URI), you must do this step to be able to inject this class like you inject other classes:
namespace App\Actions\Holding;
class UpdateMarketValuesAction
{
public function !'construct(
private readonly MarketStackService $marketStackService
) {}
}
19 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
Laravel can resolve this class only because I created a bind in the service provider. If I miss
this step, I'll get an exception because Laravel cannot resolve string values in the
constructor of the MarketStackService class.
After everything is set up correctly, we can write some actual code in the service:
class MarketStackService
{
public function !'construct(
private readonly string $uri,
private readonly string $accessKey,
private readonly int $timeout
) {}
$items = collect($response!#json('data'))
!#map(fn (array $item) !% DividendData!$fromArray($item))
!#toArray();
This method will return the dividend payouts for the given ticker symbol. As you can see, I
parse the response into a DTO, and I create a new collection from these DTOs.
Working with huge 3rd party responses can be significant pain and a source of many bugs.
By using these techniques, you can eliminate many of those bugs.
20 / 21
Martin Joo - Case Study - Portfolio And Dividend Tracker
In this case, all I need is a simple number. So there's no need to use fancy classes here.
Final Thoughts
Being a small application, it's relatively easy to design and build this portfolio tracker. But
as you have seen, there are always several interesting decisions we need to make, and
there are always opportunities to improve the code's overall quality.
In this particular case, there were two techniques I can help you with:
Custom collections.
Integrating APIs.
I hope you picked some new stuff here that you can use in your day-to-day job. This was
our mini case study; I hope you enjoyed it. Feel free to browse the source code and if you
have any questions, just reach out to me on Twitter or e-mail me at [email protected].
21 / 21