1,053 karma · joined April 6, 2013
Please feel free to contact me at any time at me@ricklubbers.nl.
Some odd areas of expertise and/or interest of mine are:
- Intermodal container shipping, container terminals,
warehousing, and logistics
- ORMs and data access libraries (author of https://github.com/Atrocit/farstorm/)
- Machine learning, object detection / OCR / LLM finetuning
- Aviation: flight crash analysis, crew resource
management, and flight sim / VATSIM
- Infrared signal processing
- Audio recognition and processing
- Smart home protocols
Disclaimer: anything I post on here is my personal opinion, not that of the company I own, nor any of the companies I've been previously employed at. Warranty provided to the front door. Batteries not included. Your Milage May Vary. Any resemblance to real persons, living or dead, is purely coincidental.[ my public key: https://keybase.io/superice; my proof: https://keybase.io/superice/sigs/ZadLKdQHi5V4vxhvflOVR6c-p_nQ2XqfMI9YF9sWSD4 ]
Also while shipping is a competitive market, the market for ports is not. You're either in a location or not. There are not hundreds of container terminals in a single port in competition because of economies of scale.
(The market for trucking companies IS competitive however, meaning that if you have to err on 'protecting' either party, you should probably pick that one)
Loading operations are much more variable, especially if your yard is not stacked well and you need to 'dig out' specific containers. If you run out of containers underneath your crane, your operations are stalled until the terminal vehicles catch up and bring you new boxes to load.
As a general rule, container ships are unloaded tier-by-tier, breadth-first if you will, not shaft-by-shift (depth-first), so this is not much of a problem in practice.
That does start to change if you want to do simultaneous loading and unloading operations, then you'd want to clear out a vertical shafts first so you can start loading operations as quickly as possible. Which is one of the many reasons dock workers hate that style of operations.
Long story short: the container terminal will always opt to please their customer (shipping line) over their non-customer (trucking companies). Truck appointment systems are usually used to force transporters to smooth out peak times not in the name of efficiency, but rather to lower the amount of dock workers the container terminal needs to hire. The truck companies generally end up footing the bill for this, both in increased workload and in detention/demurrage costs because they can't get their containers out and back in time. This money goes directly into the pocket of both the shipping line and container terminals as this is typically something they make heavy profits on.
Be very wary when container terminals and shipping lines start to push for centrally mandated appointment systems. They are much more consolidated than hinterland transport operators. I'm all for increasing efficiency but let's not even further increase market power for shipping lines and container terminals please.
Import cargo is annoying in that it is mostly random access on pickup. For pickup by train, barge, or feeder ship, a vast minority, you typically don't have cargo manifests until a day or two before pickup at best, so in practice this is also random access-ish. The customs processes are also trickier.
My experience is mostly in Rotterdam and Antwerp, and I'd say the problems in the US probably don't have to do much with automation. Rotterdam and Antwerp have very different automation levels at the biggest container terminals, yet productivity is quite similar.
There is lots of low hanging fruit in optimizing operations, like more collaborative stowage planning, simultaneous unloading and loading operations, and 'modal shifting' from road to rail and water combined with early preannouncement of manifests for trains and barges.
Disclaimer: I'm in the business of consulting and building software for container terminals, so I'll generally be biased towards those solutions.
It works surprisingly well, and is pretty resilient since it essentially acts as a message queue too. Then again, this is all low traffic stuff in a SME / B2B setting, with zero multitenancy involved.
I’ll still take a proper API or message queue any day though.
Edit: I suppose our biggest benefit is that our customer can actually change the interface with us fairly quickly. Then have database experts in-house, but devs who could do APIs. So the collaboration has been mostly smooth an account of that, and that is a huge advantage compared to them having to outsource any API work. Technically speaking I’m not a fan, but the non-technical results have been useful.
While writing software in-house gives you software better tailored to your exact situation it is at the cost of losing the overview of the business and the perspective of the parties you interact with. Whether that trade-off is worth it I’m probably pretty biased on, so take my stance with a grain of salt, but imo the business is intrinsically relatively ill suited for in-house dev considering the high degree of collab with other parties.
Apologies if it came off as arrogance, that was not what I was going for.
Your biggest pain point is most likely that you know your business very well, but you probably do not know enough about the business context of your partners. If you are running a small-ish container terminal for example, you really should know how the depot department of the major carriers are managing their empty stock so you can decide your stacking strategies in the yard. You should probably also have an understanding of benefits vs drawbacks of tower cranes vs gantry cranes found at bigger quays. That then influences what your TOS should be capable of. Same thing goes for intermodal transport planning, you really should know not only how detention/demurrage works, but also what tricks you can pull to optimize for it, and how shipping lines monitor this internally. If you are building stuff in-house, you're inevitably going to be limited in your understanding of the wider market. Short term that's a huge improvement (more tailored software, better flexibility), long term that might cause huge issues.
I can certainly understand the frustration with third-party products. Your description of 'they are understaffed and the platform is stagnant' could realistically apply to pretty much any software provider in this space. There is a huge opportunity for somebody to swoop in and build something, the bar for this sector is mostly something that doesn't straight up suck. And I'm going to be honest: we're trying to be that somebody.
If you'd like to talk, my contact details are in my profile. I'm happy to show you what we're building, and tell you what that is like behind the scenes. If you go ahead with your plan to in-house it, let's stay in contact, perhaps we can learn from eachother.
The design philosophies behind Tailwind are sound, but like so many I dislike the technical implementation. I feel like there is a lot of halo effect happening with Tailwind where judgement of technical merits is influenced by judgement on visual merits.
As an employee at a terminal, you have a) no reason to trust some software people who may have never seen one of them steel boxes up close, b) if this works, you or your colleague gets laid off, and c) if it doesn't work, you're now stuck cleaning up the mess the computer will inevitably make for you, which is way less fun than planning things yourself.
The 5%-10% invoices being incorrect is probably on the low end, I'd probably say it's 5-10% in total invoice VALUE. Because of the sheer amount of containers, you need automated invoice calculation. But if you miss an edge case ("We are allowed to invoice a 5 buck surcharge if the container is filled with at least 100kg of explosive goods, AND was loaded during the night shift because dangerous goods handling requirements now require us to have a safety officer on call, which we normally don't have during the night shift") you are leaving serious amounts of money uninvoiced, and you will never know. One of the reasons not that much effort goes into optimization algorithms is that there is so much low hanging fruit left on getting your invoices out correctly...
You have to realize that these terminals are typically a lot smaller in terms of manpower than you might think. A terminal handling 1 million TEU/year may operate with 2-3 crews in a 16 hour/day schedule for handling ships and trucks, plus some extra people handling the empties. Let's say it's 60 dock workers a day. A typically office might only employ about 12 people in overhead: one for customs/quality stuff, one for invoicing and finance, one HR, a bunch of order entry / customer service people, and 2 or 3 yard/berth/stowage planners.
Unless you look at the biggest of biggest terminals, you have to essentially regard them as SMEs in a very non-sexy business, and they treat tech and fundamental research into optimization accordingly.
As somebody actively involved in the space, there are lots of ways you can make life easier for yourself. Planning in blocks per hatch cover, grouping containers by destination, size, and weight and treating those as mostly interchangeable are table stakes. You then want to send a plan from the ship to the terminal before start of operations, so the terminal can also optimize and shuffle given they know the position of containers in the yard.
Stowage planning is a lot easier if you decide you're going to ignore the details of each container and only occupy yourself with the groups. The end result is very similar for way less work, and you give a lot more flexibility to the terminal to optimize operations.
All of the problems you can neatly divide and name are interrelated in practice, and trying to have algorithms "magically" figure it out for users is pretty much guaranteed to fail. This business eats software hotshots who think they can swoop in and "just" run some algos, "because it's just a TSP / Constraint Solver / [insert your approach here] right?". This business can definitely use bright minds, but start humbly, and start by talking to real world users please.
You don't appear to have any contact details on your profile, but if you ever would like to exchange thoughts with another company in this business, shoot me an e-mail. I think we're doing some interesting stuff for the <1 million TEU/year segment of terminals, especially if they're heavy into multi modal.
I’m sure N+1 is not a big problem in those use cases, but saying that thereforr the problem doesn’t exist is overgeneralizing a bit. B2B is not a small market.
The bigger question is: what would that solve? Caching is mostly relevant if you are repeatedly executing the same, relatively heavy, query. Load balancing is mostly interesting if you have read-only queries that you can easily execute against any of X replicas, in which case you might as well do that application side instead of having some transparent layer in between. And I don't get why you'd need cookies for something that is a persistent connection instead of individual requests?
The tabular data thing is tricky though, you're right. The impedance mismatch between tables and nested object structures isn't easy to solve.
The reality of the situation is that ODBC isn't going away any time soon, and proposing a breaking change on top of protocols like that is not realistically going to see lots of adoption unless you come up with a really great alternative AND some good marketing around that.
I've come to the conclusion that the best way to spend my efforts is to work with the ecosystem and build tools that try and fix the impedance mismatch is easier than hoping for something better to come along. Making it easier to write performant code is yielding more results for me than trying to adapt SQL-protocols to horizontally scale. A single box with some well placed indexes and well written queries is ridiculously fast.
It aligns pretty closely to what we do internally. We don't use classes or annotations to do entity definitions, instead we'll have something like this:
const User = {
fields: {
id: defineIdField(),
email: defineField('string', false), // false = NOT NULL
},
manyToOne: {
role: { entity: 'Role', nullable: false },
},
};
Then the way to query and save is something like this: datasource.inTransaction(async (orm) => {
const users = await orm.findMany('User', { where: sql`...`, orderBy: sql`email asc` });
// Await in this loop might look bad, but only blocks for the first user
for (const user of users) {
console.log(user.email, (await user.role).name);
}
const adminRole = await orm.findOne('Role', 1); // by ID, throws if not found
return orm.saveMany('User', users.map(u => ({ ...u, role: adminrole }));
});
Which looks different from what Joist is doing, but the fetching mechanics under the hood are very similar I think. Our 'scope' is just somewhat more explicit, since we don't use the event loop mechanics of JS but rather use the inTransaction callback as the boundary.(The reason we haven't open sourced this yet is that I'd like to iron out some of the ugly parts first, like the string entity identifiers, and the forced usage of an id column, and the lack of type safe where clauses right now)
N+1 just isn't cutting it for that use case, because while a manifest may be paginated, you pretty much just need your entire stowage plan in memory to do any useful operation on it.
If you look at the end-to-end problem of 'what is the minimum amount of data I need during this request' vs 'how much data do I fetch, and what is my total latency / number of roundtrips to the db doing so?' I think for most ORM patterns that use lazy loading your primary target is reducing roundtrips, and for most hand rolled queries or ORMs tweaked to do eager loading, the primary target is deduplicating the results.
My take on this is that a decent approximation is a query per relation you're fetching, so if you have 10 entities A in a transaction, and each has 20 entities B attached, ideally you want 2 queries: one for the 10 entities A, and one for the 200 entities B. Lazy loading will give you 1 query for A + 10 queries for B, and eager loading will duplicate the 10 A entities data 20 times each (and that problem gets worse as your graph gets bigger with more one-to-many relations).
Once you run into the raw data transfer between database and backend being the limit, trying to optimize that protocol comes into play, but at least in the use cases I tend to have this is not usually a bottleneck. Besides, I'll typically serialize the data fetched to send out over HTTP again, which essentially has the same challenges if you're not using protobuf or so.
There are very real reasons why writing raw SQL is a pain. You can make arguments about typesafety of queries, maintainability, mapping into application level data structures/types yadayada. Imho the primary argument is that you cannot efficiently write a query that doesn't suffer from duplication of data. If you have an entity A that has many B's, and that same entity A also has many C's, then if you join on both you now are loading A * B * C rows. This is going to be slow too, and is difficult to detangle.
ORMs in their current form are terrible at tackling this problem, and people are right to call them out for that. "Just" lazy loading or "just" eager loading both do not scale, nor does doing firing event listeners on a per-entity basis. But rawdogging SQL is not a viable solution either, and I wish people advocating that would stop this attitude of 'oh you prissy dev using your puny ORM, not willing to write a line of SQL, look at me with my glorious hair on my chest and superior intellect writing pristine queries'.
Perhaps I missed Django, but as far as I could tell, Hibernate and jooq can’t do this, Ecto can’t do it, ActiveRecord can’t, and the entirety of ORMs for JS can’t (TypeORM, Prisma, Drizzle, Sequelize and a bunch more).
Our solution was to write our own ORM-like system that “remembers” which entities you’ve loaded within the context of the transaction, and then will fetch the relation for all of them. So if you access a1.b then it will also fetch a2.b and cache it if you loaded a2 within that same transaction. The call to a2.b will then resolve instantly. So instead of n queries with n being the number of entities loaded in lazy loading, you’re doing n queries with n being the number of relations touched.
The one bad failure case is if you update entities one-by-one but also accessing their relations in a loop since you invalidate the cache for that entity type on every write.
function expectType<A, B extends A>() {}
expectType<never, typeof yourUnion>();
The function call will fail at compile time if yourUnion is anything more than never, which you can use in your else case of if statements that narrow the discriminated union.
When React started to gain traction, everybody scoffed at them. JSX was weird, uni-directional data flow was weird, ES6 classes were weird, too many building steps were weird. But it quickly became clear that this was the way forward. Traditional MVC approaches quickly failed once you got out of the example code and into more complex use cases. Which brings me to my point: React was better IF-AND-ONLY-IF your webapp needed to do more than basic forms.
React was never meant for consumer websites. It was never meant to be the default tool for anything on the web. If you wrote a web application, something complex, something with forms, planboards, custom UI, then yeah, it would be a sensible choice. It was meant to solve the problem that doing two-way binding of data is incredibly difficult if you have anything more than some standard form components.
The reality of the situation is that innovation in frontend websites (as opposed to webapps) has mostly been standing still, as everybody just switched to using React. CSS has gotten much better, but doing something like a form rendered with PHP + some validation with JS is still a pretty painful experience. HTMX is probably the best attempt I've seen, along with Elixir and the Phoenix LiveViews.
Most of the effort seems to go into writing a bunch of libraries for React to solve the complexities of having to deal with a complex set of tools for writing complex webapplications. These abstractions tend to either be complex themselves, or are leaky.
I realize my next sentence might as well be 'get off my lawn', but I really hope that devs stop focussing on writing the next state management framework, or routing library, or even rewriting existing things, and just try something to actually move dev forward. Open source a kit of sensible standard components including styling for webapps. Write a simple JS tool for some form validation. Show your barebones NodeJS app rendering HTML and processing forms in an PHP-esque way, give it a logo and a fancy name, chuck it up on GitHub. Don't try and combat complexity with more complexity. We have all the tools we need to write complex webapps. Please make simple websites easier, so people can stop complaining about the complexity of the complex tools meant for complex use cases that they don't need for their simple case.